阿里面試官都愛問的記憶體管理和GC演算法及回收策略

王下七匹狼發表於2019-07-17

JVM記憶體組成結構

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

阿里面試官都愛問的記憶體管理和GC演算法及回收策略

JVM記憶體回收

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

阿里面試官都愛問的記憶體管理和GC演算法及回收策略

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,加快記憶體回收。

  2. 物件池技術如果生成的物件是可重用的物件,只是其中的屬性不同時,可以考慮採用物件池來較少物件的生成。如果有空閒的物件就從物件池中取出使用,沒有再生成新的物件,大大提高了物件的複用率。

  3. JVM調優通過配置JVM的引數來提高垃圾回收的速度,如果在沒有出現記憶體洩露且上面兩種辦法都不能保證JVM記憶體回收時,可以考慮採用JVM調優的方式來解決,不過一定要經過實體機的長期測試,因為不同的引數可能引起不同的效果。如-Xnoclassgc引數等。

垃圾物件的判定

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()方法中讓該物件重新引用鏈上的任何一個物件建立關聯即可。而如果物件這時還沒有關聯到任何鏈上的引用,那它就會被回收掉。

垃圾收集演算法

判定除了垃圾物件之後,便可以進行垃圾回收了。下面介紹一些垃圾收集演算法,由於垃圾收集演算法的實現涉及大量的程式細節,因此這裡主要是闡明各演算法的實現思想,而不去細論演算法的具體實現。

標記—清除演算法

標記—清除演算法是最基礎的收集演算法,它分為“標記”和“清除”兩個階段:首先標記出所需回收的物件,在標記完成後統一回收掉所有被標記的物件,它的標記過程其實就是前面的根搜尋演算法中判定垃圾物件的標記過程。標記—清除演算法的執行情況如下圖所示:

阿里面試官都愛問的記憶體管理和GC演算法及回收策略

該演算法有如下缺點:

  • 標記和清除過程的效率都不高。
  • 標記清除後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致,當程式在以後的執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不觸發另一次垃圾收集動作。
複製演算法

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

  • 每次只對一塊記憶體進行回收,執行高效。
  • 只需移動棧頂指標,按順序分配記憶體即可,實現簡單。
  • 記憶體回收時不用考慮記憶體碎片的出現。

它的缺點是:可一次性分配的最大記憶體縮小了一半。 複製演算法的執行情況如下圖所示:

阿里面試官都愛問的記憶體管理和GC演算法及回收策略

但一般不用按1:1劃分記憶體空間,可以分成一個大的eden和兩塊小的survivor。

標記—整理演算法

老年代中,物件存活率比較高,如果執行較多的複製操作,效率將會變低,所以老年代一般會選用其他演算法,如標記—整理演算法。該演算法標記的過程與標記—清除演算法中的標記過程一樣,但對標記後出的垃圾物件的處理情況有所不同,它不是直接對可回收物件進行清理,而是讓所有的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。標記—整理演算法的回收情況如下所示:

阿里面試官都愛問的記憶體管理和GC演算法及回收策略

分代收集

當前商業虛擬機器的垃圾收集都採用分代收集來管理記憶體,它根據物件的存活週期的不同將記憶體劃分為幾塊,一般是把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文件)

阿里面試官都愛問的記憶體管理和GC演算法及回收策略

相關文章