阿里面試官都愛問的內存管理和GC算法及回收策略

JVM內存組成結構

JVM棧由堆、棧、本地方法棧、方法區等部分組成,結構圖以下所示:java

JVM內存回收

Sun的JVMGenerationalCollecting(垃圾回收)原理是這樣的:把對象分爲年青代(Young)年老代(Tenured)持久代(Perm),對不一樣生命週期的對象使用不一樣的算法。(基於對對象生命週期分析)面試

1.Young(年輕代)

年輕代分三個區。一個Eden區,兩個Survivor區。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當這個Survivor去也滿了的時候,從第一個Survivor區複製過來的而且此時還存活的對象,將被複制年老區(Tenured。須要注意,Survivor的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從Eden複製過來對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor去過來的對象。並且,Survivor區總有一個是空的。算法

2.Tenured(年老代)

年老代存放從年輕代存活的對象。通常來講年老代存放的都是生命期較長的對象。數組

3.Perm(持久代)

用於存放靜態文件,現在Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。持久代大小經過-XX:MaxPermSize=進行設置。架構

舉個例子:當在程序中生成對象時,正常對象會在年輕代中分配空間,若是是過大的對象也可能會直接在年老代生成(據觀測在運行某程序時候每次會生成一個十兆的空間用收發消息,這部份內存就會直接在年老代分配)。年輕代在空間被分配完的時候就會發起內存回收,大部份內存會被回收,一部分倖存的內存會被拷貝至Survivor的from區,通過屢次回收之後若是from區內存也分配完畢,就會也發生內存回收而後將剩餘的對象拷貝至to區。等到to區也滿的時候,就會再次發生內存回收而後把倖存的對象拷貝至年老區。測試

一般咱們說的JVM內存回收老是在指堆內存回收,確實只有堆中的內容是動態申請分配的,因此以上對象的年輕代和年老代都是指的JVM的Heap空間,而持久代則是以前提到的MethodArea,不屬於Heap。線程

關於JVM內存管理的一些建議

  1. 手動將生成的無用對象,中間對象置爲null,加快內存回收。3d

  2. 對象池技術若是生成的對象是可重用的對象,只是其中的屬性不一樣時,能夠考慮採用對象池來較少對象的生成。若是有空閒的對象就從對象池中取出使用,沒有再生成新的對象,大大提升了對象的複用率。指針

  3. JVM調優經過配置JVM的參數來提升垃圾回收的速度,若是在沒有出現內存泄露且上面兩種辦法都不能保證JVM內存回收時,能夠考慮採用JVM調優的方式來解決,不過必定要通過實體機的長期測試,由於不一樣的參數可能引發不一樣的效果。如-Xnoclassgc參數等。cdn

垃圾對象的斷定

Java堆中存放着幾乎全部的對象實例,垃圾收集器對堆中的對象進行回收前,要先肯定這些對象是否還有用,斷定對象是否爲垃圾對象有以下算法:

引用計數算法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1,任什麼時候刻計數器都爲0的對象就是不可能再被使用的。

引用計數算法的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的選擇,當Java語言並無選擇這種算法來進行垃圾回收,主要緣由是它很難解決對象之間的相互循環引用問題。

根搜索算法

**Java和C#**中都是採用根搜索算法來斷定對象是否存活的。這種算法的基本思路是經過一系列名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,就證實此對象是不可用的。在Java語言裏,可做爲GC Roots的兌現包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中的類靜態屬性引用的對象。
  • 方法區中的常量引用的對象。
  • 本地方法棧中JNI(Native方法)的引用對象。

實際上,在根搜索算法中,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行根搜索後發現沒有與GC Roots相鏈接的引用鏈,那它會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行。若是該對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue隊列中,並在稍後由一條由虛擬機自動創建的、低優先級的Finalizer線程去執行finalize()方法。finalize()方法是對象逃脫死亡命運的最後一次機會(由於一個對象的finalize()方法最多隻會被系統自動調用一次),稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是要在finalize()方法中成功拯救本身,只要在finalize()方法中讓該對象從新引用鏈上的任何一個對象創建關聯便可。而若是對象這時尚未關聯到任何鏈上的引用,那它就會被回收掉。

垃圾收集算法

斷定除了垃圾對象以後,即可以進行垃圾回收了。下面介紹一些垃圾收集算法,因爲垃圾收集算法的實現涉及大量的程序細節,所以這裏主要是闡明各算法的實現思想,而不去細論算法的具體實現。

標記—清除算法

標記—清除算法是最基礎的收集算法,它分爲「標記」和「清除」兩個階段:首先標記出所需回收的對象,在標記完成後統一回收掉全部被標記的對象,它的標記過程其實就是前面的根搜索算法中斷定垃圾對象的標記過程。標記—清除算法的執行狀況以下圖所示:

該算法有以下缺點:

  • 標記和清除過程的效率都不高。
  • 標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不觸發另外一次垃圾收集動做。
複製算法

複製算法比較適合於新生代,複製算法是針對標記—清除算法的缺點,在其基礎上進行改進而獲得的,它講課用內存按容量分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活着的對象複製到另一塊內存上面,而後再把已使用過的內存空間一次清理掉。複製算法有以下優勢:

  • 每次只對一塊內存進行回收,運行高效。
  • 只需移動棧頂指針,按順序分配內存便可,實現簡單。
  • 內存回收時不用考慮內存碎片的出現。

它的缺點是:可一次性分配的最大內存縮小了一半。 複製算法的執行狀況以下圖所示:

但通常不用按1:1劃份內存空間,能夠分紅一個大的eden和兩塊小的survivor。

標記—整理算法

老年代中,對象存活率比較高,若是執行較多的複製操做,效率將會變低,因此老年代通常會選用其餘算法,如標記—整理算法。該算法標記的過程與標記—清除算法中的標記過程同樣,但對標記後出的垃圾對象的處理狀況有所不一樣,它不是直接對可回收對象進行清理,而是讓全部的對象都向一端移動,而後直接清理掉端邊界之外的內存。標記—整理算法的回收狀況以下所示:

分代收集

當前商業虛擬機的垃圾收集都採用分代收集來管理內存,它根據對象的存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲新生代和老年代。在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少許存活,所以可選用複製算法來完成收集,而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。

每一個對象都有一個年齡(Age)計數器,若是對象在Eden出聲並講過一次Minor GC還存活,將被移動到Survivor區並將Age設置爲1,以後每在Survivor區中熬過一次Minor GC,Age就加1,當增長到必定程度(默認爲15),就能夠放到老年代中。

垃圾收集器

垃圾收集器是內存回收算法的具體實現,Java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣廠商、不一樣版本的虛擬機所提供的垃圾收集器均可能會有很大的差異。Sun HotSpot虛擬機1.6版包含了以下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old。這些收集器以不一樣的組合形式配合工做來完成不一樣分代區的垃圾收集工做。

垃圾回收分析

在用代碼分析以前,咱們對內存的分配策略明確如下三點:

  • 對象優先在Eden分配。當Eden沒有足夠空間分配時,將發起一次Minor GC
  • 大對象(須要大量連續空間的java對象,如長的字符串和數組)直接進入老年代。因爲新生代使用複製算法回收內存,這樣能夠避免在Eden和兩個Survivor區之間發生大量的內存複製。
  • 長期存活的對象將進入老年代。

對垃圾回收策略說明如下兩點:

  • 新生代GC(Minor GC):發生在新生代的垃圾收集動做,由於Java對象大多都具備朝生夕滅的特性,所以Minor GC很是頻繁,通常回收速度也比較快。
  • 老年代GC(Major GC/Full GC):發生在老年代的GC,出現了Major GC,常常會伴隨至少一次Minor GC。因爲老年代中的對象生命週期比較長,所以Major GC並不頻繁,通常都是等待老年代滿了後才進行Full GC,並且其速度通常會比Minor GC慢10倍以上。另外,若是分配了Direct Memory,在老年代中進行Full GC時,會順便清理掉Direct Memory中的廢棄對象。

Dalvik虛擬機使用Mark-Sweep算法來進行垃圾收集。顧名思義,Mark-Sweep算法就是爲Mark和Sweep兩個階段進行垃圾回收。其中,Mark階段從根集(Root Set)開始,遞歸地標記出當前全部被引用的對象,而Sweep階段負責回收那些沒有被引用的對象。在分析Dalvik虛擬機使用的Mark-Sweep算法以前,咱們先來了解一下什麼狀況下會觸發GC。

讀者福利、完整面試題【含答案】Java核心筆記,Java架構面試專題整合千道(pdf文檔)

相關文章
相關標籤/搜索