深入理解 Java 虛擬機 - 你瞭解 GC 算法原理嗎

點擊上方 好好學java ,選擇 星標 公衆號

重磅資訊、乾貨,第一時間送達
今日推薦:打卡活動第二期來啦,100% 能獲得獎品個人原創+1博客:點擊前往,查看更多

虛擬機系列文章

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

  • 哪些內存需要回收?

  • 什麼時候回收?

  • 如何回收?

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

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

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

引用計數算法

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

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

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

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

 1/**
 2 * @ClassName Test_02
 3 * @Description
 4 * @Author 歐陽思海
 5 * @Date 2019/12/5 16:59
 6 * @Version 1.0
 7 **/
 8public class Test_02 {
 9
10    public static void main(String[] args) {
11        Instance instanceA = new Instance();
12        Instance instanceB = new Instance();
13
14        instanceA.instance = instanceB;
15        instanceB.instance = instanceA;
16
17        instanceA = null;
18        instanceB = null;
19
20        System.gc();
21
22        Scanner scanner = new Scanner(System.in);
23        scanner.next();
24    }
25}
26
27class Instance{
28    public Object instance = null;
29}

如果使用的是引用計數算法,這是不能被回收的,當然,現在的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 總結

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


更多Java技術文章,盡在【好好學java】網站。網址:www.java1000.com  搜索 好好學java 或 閱讀原文 可達!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章