GC算法原理

JVM 垃圾回收原理

 

對於JVM的垃圾收集(GC),這是一個作爲Java開發者必須瞭解的內容,那麼,我們需要去了解哪些內容呢,其實,GC主要是解決下面的三個問題:

  • 哪些內存需要回收?

  • 什麼時候回收?

  • 如何回收?

回答了這三個問題,也就對於GC算法的原理有了最基本的瞭解。

 

1 如何判定哪些內存需要回收


  在Java虛擬機的堆中會存放着很多的對象,那麼,我們需要回收垃圾的時候,是通過什麼算法來判斷哪些垃圾的生命週期已到,需要回收呢?接下來的幾種算法將幫助你解決這幾個問題。

引用計數算法

先講講第一個算法:引用計數算法

  其實,這個算法的思想非常的簡單,一句話就是:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器加1;當引用失效時,計數器減1;任何時刻計數器爲0的對象就是不可能再被使用的

  這些簡單的算法現在是否還被大量的使用呢,其實,現在用的已經不多,沒有被使用的最主要的原因是他有一個很大的缺點很難解決對象之間循環引用的問題

  循環引用當A有B的引用,B又有A的引用的時候,這個時候,即使A和B對象都爲null,這個時候,引用計數算法也不會將他們進行垃圾回收。

public class Test_02 {

    public static void main(String[] args) {
        Instance instanceA = new Instance();
        Instance instanceB = new Instance();

        instanceA.instance = instanceB;
        instanceB.instance = instanceA;

        instanceA = null;
        instanceB = null;

        System.gc();

        Scanner scanner = new Scanner(System.in);
        scanner.next();
    }
}

class Instance{
    public Object instance = null;
}

如果使用的是引用計數算法,這是不能被回收的,當然,現在的JVM是可以被回收的。

可達性分析算法

  這個算法的思想也是很簡單的,這裏有一個概念叫做可達性分析,如果知道圖的數據結構,這裏可以把每一個對象當做圖中的一個節點,我們把一個節點叫做GC Roots,如果一個節點到GC Roots沒有任何的相連的路徑,那麼就說明這個節點不可達,也就是這個節點可以被回收。

 

上面圖中,雖然obj7、8、9相互引用,但是到GC Roots不可達,所以,這種對象也是會被當做垃圾收集的。

在Java中,可以作爲GC Roots的對象包括以下幾種:

  • 虛擬機棧(棧幀中的局部變量表,Local Variable Table)中引用的對象。

  • 方法區中類靜態屬性引用的對象。

  • 方法區中常量引用的對象。

  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

 

2 什麼時候回收


  在可達性分析算法中不可達的對象,也不是一定會死亡的,它們暫時都處於“緩刑”階段,要真正宣告一個對象“死亡”,至少要經歷兩次標記過程。

step1:判斷有沒有必要執行finalize()方法

  • 如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行`finalize()`方法

另外,有兩種情況都視爲“沒有必要執行”:

  • 對象沒有覆蓋finaliza()方法。

  • finalize()方法已經被虛擬機調用過。

step2:如何執行

  如果這個對象被判定爲有必要執行finalize()方法,那麼此對象將會放置在一個叫做 F-Queue 的隊列中,並在稍後由一個虛擬機自動建立的、低優先級的Finalizer線程去執行它。

step3:執行死亡還是逃脫死亡

首先,我們需要知道,finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue 隊列中的對象進行第二次小規模的標記。

  • 逃脫死亡:對象想在finalize()方法中成功拯救自己,只要重新與引用鏈上的任何一個對象建立關聯即可,例如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,這樣在第二次標記時它將被移出“即將回收”的集合。

  • 執行死亡:對象沒有執行逃脫死亡,那就是死亡了。

 

3 如何回收


  如何回收其實就是利用哪些算法進行回收,垃圾收集算法這裏講幾種大家平時也是看到的比較的算法,分別爲:標記-清除算法複製算法標記-整理算法分代回收算法

  這部分的內容其實在網上的文章比較多了,而且,基本上的差別不大,所以,從網上的文章選取下來,當做一個小的總結,大家可以參考這篇文章算是一個比較全的總結:GC算法與內存分配策略。

標記-清除(Mark-Sweep)算法

  標記-清除(Mark-Sweep) 算法是最基礎的垃圾收集算法,後續的收集算法都是基於它的思路並對其不足進行改進而得到的。顧名思義,算法分成“標記”、“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,標記過程在前一節講述對象標記判定時已經講過了。

標記-清除算法的不足主要有以下兩點:

  • 空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不觸發另一次垃圾收集動作。

  • 效率問題,因爲內存碎片的存在,操作會變得更加費時,因爲查找下一個可用空閒塊已不再是一個簡單操作。

標記-清除算法的執行過程如下圖所示:

 

 

 

複製(Copying)算法

  爲了解決標記-清除算法的效率問題,一種稱爲複製”(Copying)的收集算法出現了,思想爲:它將可用內存按容量分成大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,就將還存活着的對象複製到另一塊上面,然後再把已使用過的內存空間一次清理掉。

  這樣做使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,代價可能過高了。複製算法的執行過程如下圖所示:

 

 

 

標記-整理(Mark-Compact)算法

  複製算法在對象存活率較高時要進行較多的複製操作,效率將會變低。更關鍵的是:如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用複製算法

  根據老年代的特點,標記-整理(Mark-Compact)算法被提出來,主要思想爲:此算法的標記過程與標記-清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉邊界以外的內存 具體示意圖如下所示:

 

分代收集(Generational Collection)算法

  當前商業虛擬機的垃圾收集都採用分代收集(Generational Collection)算法,此算法相較於前幾種沒有什麼新的特徵,主要思想爲:根據對象存活週期的不同將內存劃分爲幾塊,一般是把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適合的收集算法:

  • 新生代 在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。

  • 老年代 在老年代中,因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清除標記-整理算法來進行回收。

 

4 總結

這裏用思維導圖做一個小的總結。

 

參考

  堆外內存的回收機制分析 https://www.jianshu.com/p/35cf0f348275 

  java調用本地方法--jni簡介 https://blog.csdn.net/w1992wishes/article/details/80283403 

  咱們從頭到尾說一次 Java 垃圾回收 https://mp.weixin.qq.com/s/pR7U1OTwsNSg5fRyWafucA 

  深入理解 Java 虛擬機 

  Java Hotspot G1 GC的一些關鍵技術 https://tech.meituan.com/2016/09/23/g1.html

 

附:關於GC Roots的理解

作者:RednaxelaFX
鏈接:https://www.zhihu.com/question/53613423/answer/135743258
來源:知乎

所謂“GC roots”,或者說tracing GC的“根集合”,就是一組必須活躍的引用
例如說,這些引用可能包括:
  • 所有Java線程當前活躍的棧幀裏指向GC堆裏的對象的引用;換句話說,當前所有正在被調用的方法的引用類型的參數/局部變量/臨時值。
  • VM的一些靜態數據結構裏指向GC堆裏的對象的引用,例如說HotSpot VM裏的Universe裏有很多這樣的引用。
  • JNI handles,包括global handles和local handles
  • (看情況)所有當前被加載的Java類
  • (看情況)Java類的引用類型靜態變量
  • (看情況)Java類的運行時常量池裏的引用類型常量(String或Class類型)
  • (看情況)String常量池(StringTable)裏的引用

注意,是一組必須活躍的引用,不是對象。

  Tracing GC的根本思路就是:給定一個集合的引用作爲根出發,通過引用關係遍歷對象圖,能被遍歷到的(可到達的)對象就被判定爲存活,其餘對象(也就是沒有被遍歷到的)就自然被判定爲死亡。注意再注意:tracing GC的本質是通過找出所有活對象來把其餘空間認定爲“無用”,而不是找出所有死掉的對象並回收它們佔用的空間。
GC roots這組引用是tracing GC的起點。要實現語義正確的tracing GC,就必須要能完整枚舉出所有的GC roots,否則就可能會漏掃描應該存活的對象,導致GC錯誤回收了這些被漏掃的活對象。

  這就像任何遞歸定義的關係一樣,如果只定義了遞推項而不定義初始項的話,關係就無法成立——無從開始;而如果初始項定義漏了內容的話,遞推出去也會漏內容。

 

那麼分代有什麼好處?

  對傳統的、基本的GC實現來說,由於它們在GC的整個工作過程中都要“stop-the-world”,如果能想辦法縮短GC一次工作的時間長度就是件重要的事情。如果說收集整個GC堆耗時太長,那不如只收集其中的一部分?
  於是就有好幾種不同的劃分(partition)GC堆的方式來實現部分收集,而分代式GC就是這其中的一個思路。

  這個思路所基於的基本假設大家都很熟悉了:weak generational hypothesis——大部分對象的生命期很短(die young),而沒有die young的對象則很可能會存活很長時間(live long)。

  這是對過往的很多應用行爲分析之後得出的一個假設。基於這個假設,如果讓新創建的對象都在young gen裏創建,然後頻繁收集young gen,則大部分垃圾都能在young GC中被收集掉。由於young gen的大小配置通常只佔整個GC堆的較小部分,而且較高的對象死亡率(或者說較低的對象存活率)讓它非常適合使用copying算法來收集,這樣就不但能降低單次GC的時間長度,還可以提高GC的工作效率。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章