天天看點

「後端」Java 程式員必知的 GC 垃圾回收機制

作者:架構思考
之前分享過《「後端」Java 程式員必知的 JVM 基礎知識總結》和《「後端」Java 程式員必知的 G1 垃圾回收器知識總結》,有朋友表示跨度太大,是以穿插本篇文章。歡迎閱讀~

一、為什麼要進行垃圾回收

随着程式的運作,記憶體中存在的執行個體對象、變量等資訊占據的記憶體越來越多,其中有很多對象再也用不到,這些用不到的對象就被稱之為垃圾,如果不及時進行垃圾回收,必然會帶來程式性能的下降,甚至會因為可用記憶體不足造成一些不必要的系統異常。

垃圾回收機制主要是對 JVM 中堆記憶體進行管理,如果對 JVM 相關的概念還不了解,可以看一看《「後端」Java 程式員必知的 JVM 基礎知識總結》這篇文章。

二、如何判定對象是否為垃圾

1、引用計數法

給對象添加一引用計數器,被引用一次計數器值就加 1;當引用失效時,計數器值就減 1;計數器為 0 時,對象就是垃圾。

優點是執行效率高,缺點是無法解決對象之間互相循環引用的問題。

2、可達性分析算法

以 GC Roots 為起始點進行搜尋,判斷對象的引用鍊是否可達,可達的對象都是存活的,不可達的對象可被回收。GC Roots 一般包含以下内容:

  • 虛拟機棧中局部變量表中引用的對象
  • 本地方法棧中 JNI 中引用的對象
  • 方法區中類靜态屬性引用的對象
  • 方法區中的常量引用的對象

對象死亡(被回收)前的最後一次掙紮:

即使在可達性分析算法中不可達的對象,也并非是“必死不可”,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經曆兩次标記過程。

第一次标記:如果對象在進行可達性分析後發現沒有與 GC Roots 相連接配接的引用鍊,那它将會被第一次标記;

第二次标記:在第一次标記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法。在 finalize() 方法中沒有重新與引用鍊建立關聯關系的,将被進行第二次标記。

第二次标記成功的對象将真的會被回收,如果對象在 finalize() 方法中重新與引用鍊建立了關聯關系,那麼将會逃離本次回收,繼續存活。

三、回收垃圾的算法

回收垃圾的算法主要有 4 種:标記清除算法, 标記整理算法,複制算法,分代收集算法。下面分别介紹。

1、标記清除

标記:從 GC Roots 為起始點進行掃描,如果是活動對象,則程式會在對象頭部打上标記。

清除:對堆記憶體從頭到尾進行線性周遊,回收不可達對象。

「後端」Java 程式員必知的 GC 垃圾回收機制

标記清除

但是,标記清除算法會産生大量不連續的記憶體碎片,導緻無法給大對象配置設定記憶體。例如上圖中 B 與 E 之間隻剩 2 格,若有一個新對象要占用 3 格,則需要開辟另外的記憶體或者 Full GC。

2、标記整理

标記:從 GC Roots 為起始點進行掃描,如果是活動對象,則程式會在對象頭部打上标記。

整理:移動所有存活對象,且按照記憶體位址次序依次排列,然後将末端以後的記憶體位址全部回收。

「後端」Java 程式員必知的 GC 垃圾回收機制

标記整理

彌補了标記清除算法的不足,不會産生記憶體碎片。但是需要移動大量對象,處理效率比較低。

3、複制算法

将記憶體劃分為大小相等的兩塊,每次隻使用其中一塊,當這一塊記憶體用完了就将還存活的對象複制到另一塊上面,然後再把使用過的記憶體空間進行一次清理。

「後端」Java 程式員必知的 GC 垃圾回收機制

複制算法

不會産生記憶體碎片問題,順序配置設定記憶體,執行效率高,但每次隻使用了一半的記憶體,未免有點浪費。

4、分代收集

分代收集實際上就是将上述 3 種算法綜合起來,針對不同的區域,采用不同的方法,按照對象的生命周期的不同劃分區域,采用不同的垃圾回收算法,以提高 JVM 回收效率。

Java 堆分為兩部分,Java 堆 = 新生代 + 老年代,預設分别占堆空間為 1/3、2/3;其中,新生代 = Eden + From Survivor + To Survivor,預設為 8:1:1。這樣劃分是由于對象生存周期的特殊性,針對不同的對象,采用不同的方法。

「後端」Java 程式員必知的 GC 垃圾回收機制

Java 堆記憶體劃分

新生代使用:複制算法

老年代使用:标記清除 或 标記整理 算法

所有的對象都在 Eden 區建立,由于大部分對象都是“朝生夕滅”,隻有少量對象能存活下來,是以在新生代采用複制算法,隻有少量對象需要複制,這樣最劃算。

當 Eden 區滿了,那麼就會觸發一次 Young GC,也就是年輕代垃圾回收。少量有用的對象會複制到 From 區。這樣整個Eden區就被清理幹淨了,可以繼續建立新的對象。

當 Eden 區再次被用完,就再觸發一次 YoungGC,這個時候跟剛才稍稍有點差別。這次觸發 Young GC 後,會将 Eden 區與 From 區還在被使用的對象複制到 To 區,再下一次 YoungGC 的時候,則是将 Eden 區與 To 區中的還在被使用的對象複制到 From 區。

經過若幹次 YoungGC 後,有些對象在 From 與 To 之間來回遊蕩,這時候 From 區與 To 區亮出了底線(門檻值),這些家夥要是到現在還沒挂掉,對不起,一起複制老年代吧。

而在老年代,大部分對象任然會繼續存活下來,此時采用标記整理或者标記清除算法,這樣最劃算。

對象如何晉升到老年代?

1、經曆一定次數的 Minor GC 任然存活的對象,預設 15 次;

2、Eden 區或 Survivor 區域存放不下的對象;

3、新生成的大對象,直接放入老年代。

四、常見的垃圾收集器

Serial 垃圾收集器(單線程,複制算法):

Serial 是單線程收集,進行垃圾收集時必須暫停所有工作線程。但是它簡單高效,JVM Client 模式下預設的年輕代收集器。

「後端」Java 程式員必知的 GC 垃圾回收機制

串行收集器

ParNew 垃圾收集器(多線程,複制算法):

ParNew 是多線程收集器,是 CMS 預設的新生代垃圾回收器,其他行為特點與 Serial 一樣。

Parallel Scavenge 垃圾收集器(多線程,複制算法):

Parallel Scavenge 和 ParNew 一樣,都是多線程、新生代垃圾收集器。兩者的差別在于:

Parallel Scavenge 追求 CPU 吞吐量,能夠在較短時間内完成指定任務,是以适合沒有互動的背景計算;

ParNew 追求降低使用者停頓時間,适合互動式應用。

Serial Old 垃圾收集器(單線程,标記整理算法):

Serial Old 收集器是 Serial 的老年代版本,都是單線程收集器,都适合用戶端應用。它們唯一的差別就是:Serial Old 工作在老年代,使用“标記-整理”算法;Serial 工作在新生代,使用“複制”算法。

CMS 垃圾收集器(标記清楚算法):

CMS (Concurrent Mark Sweep,并發标記清除) 收集器是以擷取最短回收停頓時間為目标的收集器(追求低停頓),它在垃圾收集時,使用者線程 和 GC 線程并發執行,是以在垃圾收集過程中不會感到明顯的卡頓。

具體執行過程如下圖:初始标記 (Initial Mark) —> 并發标記 (Concurrent Mark) —> 重新标記 (Remark) —> 并發清除 (Concurrnet Sweep)。

「後端」Java 程式員必知的 GC 垃圾回收機制

并發執行

  1. 初始标記 (Initial Mark):僅僅隻是标記一下 GC Roots 能直接關聯到的對象,速度很快,需要 Stop The World。
  2. 并發标記 (Concurrent Mark):從 GC Roots 的直接關聯對象開始周遊整個對象圖的過程,耗時較長,但不需要停頓使用者線程,可與垃圾收集器線程一起并發執行。
  3. 重新标記 (Remark):該階段是為了修正并發标記期間,因使用者程式運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段需要 Stop The World,而且停頓時間通常比初始階段稍長一些,但也遠比并發标記階段的時間短。
  4. 并發清除 (Concurrnet Sweep):清理删除掉标記階段判斷已經死亡的對象,由于不需要移動存活對象,所有這個階段可以和使用者線程并發執行。

CMS 收集器是并發收集,有兩次 Stop The Words,兩次标記,因為 GC 線程和應用線程同時執行,好比你媽在打掃房間,你還在扔紙屑,可能産生新的引用關系。

CMS 的缺點:吞吐量低,無法處理浮動垃圾,導緻頻繁 Full GC,使用“标記-清除”算法産生碎片空間。

文章來源:https://www.jianshu.com/p/2e926823779d

繼續閱讀