1. 程式人生 > >jvm之java垃圾回收機制詳解

jvm之java垃圾回收機制詳解

      傳統的C/C++等程式語言,需要程式設計師負責回收已經分配出去的記憶體。顯示進行垃圾回收是一件令人頭疼的事情,因為程式設計師並不總是知道記憶體應該何時進行釋放。如果一些分配出去的記憶體不能及時的回收就會引起系統執行速度下降,甚至導致程式癱瘓,這種現象稱為記憶體洩露。

      與C/C++語言不同,Java語言不需要程式設計師自己去控制記憶體回收,Java程式的記憶體分配和回收都是由JVM在後臺自動進行的。JRE會負責回收那些不再使用的記憶體,這種機制被稱為垃圾回收機制(Garbage Collection,也被稱為GC)。通常JVM會提供一個後臺執行緒來進行檢測和控制,一般都是在CPU空閒或者記憶體不足時自動進行垃圾回收。

     Java語言規範沒有明確地說明JVM使用哪種垃圾回收演算法,但是任何一種垃圾回收演算法一般要做2件基本的事情:(1)發現無用資訊物件;(2)回收被無用物件佔用的記憶體空間,使該空間可被程式再次使用。

     1)如何確定一個物件是否可以被回收

     引用計數演算法:

 引用計數演算法是通過判斷物件的引用數量來決定物件是否可以被回收。在這種方法中,堆中的每個物件例項都有一個引用計數器。當一個物件被建立時,且將該物件例項分配給一個引用變數,該物件例項的引用計數設定為 1。當任何其它變數被賦值為這個物件的引用時,物件例項的引用計數器加 1(a = b,則b引用的物件例項的計數器加 1,因為b被引用),但當一個物件例項的某個引用超過了生命週期或者被設定為一個新值時,物件例項的引用計數器減 1。特別地,當一個物件例項被垃圾收集時,它引用的任何物件例項的引用計數器均減 1。任何引用計數器為0的物件例項可以被當作垃圾收集。其優點是執行速度快,缺點是很難解決物件之間相互迴圈引用的問題。

     可達性分析演算法

可達性分析演算法是通過判斷物件的引用鏈是否可達來決定物件是否可以被回收。JVM把所有的引用關係看作一張圖,通過一系列的名為 “GC Roots” 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個物件不可達)時,則證明此物件是不可用的。如下圖所示:

在上圖中可以看出,紅色的物件可以被回收。結合上圖,此演算法不難理解,GC ROOT實際上為“有用的物件”如果某一個物件沒有被有用的物件間接或直接的引用,則其是無用的,可以被回收掉。

    2)如何回收垃圾

    標記清除演算法

標記-清除演算法分為標記和清除兩個階段。該演算法首先從根集合進行掃描,對存活的物件物件標記,標記完畢後,再掃描整個空間,未被標記的物件將被回收。標記-清除演算法的劣勢是,標記和清除兩個過程的效率都不高;標記-清除演算法不需要進行物件的移動,並且僅對不存活的物件進行處理,因此標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。如下圖:

           

                

    上圖為回收前,下圖為回收後。在掃描一遍為,紅色的為不存活物件,白色的為無資料,綠色的為存活物件,由圖可見,此演算法產生了大量的記憶體碎片。

    複製演算法

複製演算法將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這種演算法適用於物件存活率低的場景,比如新生代。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。且劣勢為需要50%的記憶體空間進行垃圾回收。現在商用的虛擬機器都採用這種演算法來回收新生代。如下圖:

           

   由上圖可見,此演算法將存活的物件複製到另一塊記憶體,再將此記憶體全部清除(此記憶體便為空了),下次垃圾回收,再將另一塊記憶體的存活物件複製到此記憶體,以此迴圈進行垃圾回收。

   標記整理演算法

   複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。標記整理演算法的標記過程類似標記清除演算法,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,類似於磁碟整理的過程,該垃圾回收演算法適用於物件存活率高的場景(老年代)。其不會產生記憶體碎片。

同樣是第一例的垃圾圖,回收後如下:

             

可見此演算法記憶體碎片減少。

   分代收集演算法

   對於一個大型的系統,當建立的物件和方法變數比較多時,堆記憶體中的物件也會比較多,如果逐一分析物件是否該回收,那麼勢必造成效率低下。分代收集演算法是:不同的物件的生命週期(存活情況)是不一樣的,而不同生命週期的物件位於堆中不同的區域,因此對堆記憶體不同區域採用不同的策略進行回收可以提高 JVM 的執行效率。當代商用虛擬機器使用的都是分代收集演算法:新生代物件存活率低,就採用複製演算法;老年代存活率高,就用標記清除演算法或者標記整理演算法。Java堆記憶體一般可以分為新生代、老年代和永久代三個模組。

   下面做進一步說明:

   1)新生代

 新生代的目標就是儘可能快速的收集掉那些生命週期短的物件,一般情況下,所有新生成的物件首先都是放在新生代的。新生代記憶體按照 8:1:1 的比例分為一個eden區和兩個survivor(survivor0,survivor1)區,大部分物件在Eden區中生成。在進行垃圾回收時,先將eden區存活物件複製到survivor0區,然後清空eden區,當這個survivor0區也滿了時,則將eden區和survivor0區存活物件複製到survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後交換survivor0區和survivor1區的角色(即下次垃圾回收時會掃描Eden區和survivor1區),即保持survivor1區為空,如此往復。特別地,當survivor1區也不足以存放eden區和survivor0區的存活物件時,就將存活物件直接存放到老年代。如果老年代也滿了,就會觸發一次FullGC,也就是新生代、老年代都進行回收。注意,新生代發生的GC也叫做MinorGC,MinorGC發生頻率比較高,不一定等 Eden區滿了才觸發。如下圖所示:

  2)老年代

  老年代存放的都是一些生命週期較長的物件,就像上面所敘述的那樣,在新生代中經歷了N次垃圾回收後仍然存活的物件就會被放到老年代中。此外,老年代的記憶體也比新生代大很多(大概是兩倍),當老年代滿時會觸發Full GC,老年代物件存活時間比較長,因此FullGC發生的頻率比較低。

 3)永久代

  永久代主要用於存放靜態檔案,如Java類、方法等。永久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如使用反射、動態代理、CGLib等bytecode框架時,在這種時候需要設定一個比較大的永久代空間來存放這些執行過程中新增的類。

其中:

Minor GC為對新生代進行回收,不會影響到年老代。因為新生代的 Java 物件大多死亡頻繁,所以 Minor GC 非常頻繁,一般在這裡使用速度快、效率高的演算法,使垃圾回收能儘快完成。Full GC為對整個堆進行回收,包括新生代、老年代和永久代。由於Full GC需要對整個堆進行回收,所以比Minor GC要慢,因此應該儘可能減少Full GC的次數,導致Full GC的原因包括:老年代被寫滿、永久代(Perm)被寫滿和System.gc()被顯式呼叫等。

關於新生代和老年代晉升的說明:

      大物件直接進入年老代

      大物件即需要大量連續記憶體空間的Java物件,如長字串及陣列。經常出現大物件導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來安置他們。  

      長期存活的物件將進入年老代

      JVM給每個物件定義了一個物件年齡計數器,物件在Eden建立並經過第一次Minor GC後仍然存活,並能被Suivivor容納的話,物件年齡加1。每經歷過一次Minor GC,年齡就增加1歲,當到一定年齡(預設15歲,可以通過引數-XXMaxTenuringThreshold設定),新生代就將會晉升年老代。

      動態物件年齡判定

      為了更好地適應不同程式記憶體狀況,JVM並不硬性要求物件年齡達到MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡的所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入年老代。