一文讀懂系列-JVM垃圾收集

weixin_34208283發表於2018-05-18

之前的一篇文章介紹了JVM的記憶體區域劃分和每個記憶體區域的功能,今天就介紹下JVM中的垃圾收集的相關演算法和常用垃圾收集器。

哪些物件要收集

進行垃圾回收的前提是要先判斷出JVM中哪些物件需要回收,那麼我們先介紹下判斷物件已經沒有引用的常用演算法。

1)引用計數法:這個演算法很簡單,就是給每個物件增加一個引用計數器,在引用它的地方計數器就加1,當引用失效就減1,當計數器為0的時候就可以進行回收了。但是這個演算法的最大缺點就是無法處理迴圈引用的問題,會導致物件一直無法回收。

2)GC Roots 根搜尋法: 這個演算法的是通過定義為GC Roots的物件作為起點,從這些節點向下搜尋,搜尋走過的路徑為引用鏈,當一個物件到GC Roots沒有任何引用鏈時,則證明這些物件時不可用的,GC可以進行回收。

下列4種物件可作為GC Roots:

  • 虛擬機器棧幀中的變數表中引用的物件
  • 方法區中的類靜態屬性引用的物件
  • 方法區中的常量引用的物件
  • native方法棧中引用的物件

垃圾收集演算法

JVM通過GC Roots進行失效物件查詢,對於不在引用鏈上的物件會根據垃圾回收演算法進行回收,JVM常用的垃圾回收主要有三種標記清除演算法、標記整理演算法、複製演算法,對於JVM實際使用過程中使用的其實是按照堆中不同的記憶體區域特性(新生代、老年代)採用不同的演算法進行回收,也就是常說的分代回收演算法,下面分別介紹下這幾個演算法。

1)標記清除演算法
標記清除演算法是用的比較多的一種垃圾收集演算法,原理也很簡單,根據GC Roots自上而下的搜尋到的物件引用鏈,在堆中進行查詢,找出不在引用鏈上的物件,然後進行標記,圖中用黃色標出,標記後由GC統一對這些標記記憶體區域進行資料清除。
標記清除演算法雖然很簡單,但是缺點也很明顯,那就是記憶體中存在較多的碎片,後續再分配堆空間的時候無法有效分配連續的記憶體空間,導致一些大物件可能無法申請記憶體,總體記憶體利用率較低,優點是回收速度快。


4720632-7421ba35737e84c2.png
Screenshot 2018-05-18 08.57.09.png

2)標記整理演算法
標記整理演算法是標記清除演算法的改進版,和標記清除演算法一樣先進行根據引用鏈進行標記,再由GC對標記後的記憶體區域進行清除,下面就是和標記清除法不一樣的地方了,標記整理演算法為了提高連續記憶體的利用率會進行使用記憶體區域的整理,將使用的記憶體區域進行連續性壓縮,類似PC上的磁碟整理,一方面可以提高記憶體的連續讀效能,另一方面提高了記憶體利用率,缺點就是效能會略低於標記清除演算法。

4720632-44a80ec1f728e484.png
Screenshot 2018-05-18 08.57.20-w300

3)複製演算法
複製演算法是目前JVM新生代常用的演算法,其實原理也很簡單,將記憶體分配為兩塊同等大小的區域,分配記憶體的時候使用區域一,區域二空著,當要進行記憶體回收的時候,將區域一中已經使用的記憶體全部複製到區域二中,最後再將區域一中的記憶體全部清除,這樣清理的速度是非常快的,GC回收效能是最好的,但是缺點是記憶體可使用空間少了一半。


4720632-294423a67a94ddd5.png
Screenshot 2018-05-18 08.57.42-w300

因為記憶體空間可使用率低的問題,IBM和Sun都對垃圾回收的複製演算法進行了改良。IBM和Sun在研究中發現新生代中的98%物件都是“朝生夕死”的,所以並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體分為一塊比較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是8:1,也就是說,每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的空間會被浪費。

當然,98%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴於老年代進行分配擔保,所以大物件直接進入老年代。

4)分代收集演算法
介紹了上面三種常用的垃圾收集演算法,其實在JVM中根據不同的記憶體區域,這幾種垃圾回收演算法都有用到,JVM聰明的按照每個記憶體區域的特性,選用了不同的垃圾收集演算法。之前的一篇JVM記憶體區域的文章中介紹了堆中分為新生代和老年代,新生代中的物件存活時間非常短,但是對垃圾回收時間要求非常高,需要高效能回收,所以才用了複製演算法,並將新生代分為了Eden、 From Survior、To survior三個區域;對於老年代因為資料存活時間較長,對收集效能沒那麼敏感,所以採用了標記清除和標記整理演算法,這就是我們常說的JVM分代收集演算法。

垃圾收集器

1)Serial收集器
Serial收集器對於執行在Client模式下的虛擬機器來說是一個很好的選擇。

這個收集器是一個單執行緒的收集器,但它的單執行緒的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。收集器的執行過程如下圖所示:


4720632-740717f55fe46cf4.png
Screenshot 2018-05-18 09.58.20

JVM啟動引數設定:-XX:+UseSerialGC
新生代、老年代都會使用序列回收
新生代複製演算法
老年代標記-整理

2)ParNew收集器
ParNew收集器其實就是Serial收集器新生代的並行版本。


4720632-0b9bdeee262f1673.png
Screenshot 2018-05-18 09.59.45

JVM啟動引數設定:-XX:+UseParNewGC
新生代並行
老年代序列

3)Parallel Scavenge收集器
類似ParNew,但更加關注吞吐量。
JVM啟動引數設定:-XX:+UseParallelGC 新生代並行
老年代序列

4)Serial old收集器

5)Parallel Old收集器
Parallel Old收集器是Parallel Scanvenge收集器的老年代版本。
JVM啟動引數設定:-XX:+UseParallelGC
新生代並行
老年代並行
[圖片上傳失敗...(image-cb5b43-1526614128569)]

6)CMS收集器
上面介紹的所有收集器,當執行GC時,都會stop the world,但是下面的CMS收集器卻不會這樣。
CMS收集器(Concurrent Mark Sweep:併發標記清除)是一種以獲取最短回收停頓時間為目標的收集器。適合應用在網際網路站或者B/S系統的伺服器上,這類應用尤其重視伺服器的響應速度,希望系統停頓時間最短。

JVM啟動引數設定:-XX:+UseConcMarkSweepGC
老年態並行標記-清除演算法

4720632-980810d81b65af8e.png
Screenshot 2018-05-18 10.07.09

整個過程中初始標記和重新標記時,需要stop the world,但是這兩個標記時間都非常短,可以忽略不計。

  • 初始標記

GC Roots可以直接關聯到的物件

速度快

  • 併發標記(和使用者執行緒一起)

主要標記過程,標記全部物件

  • 重新標記

由於併發標記時,使用者執行緒依然執行,因此在正式清理前,再做修正

  • 併發清除(和使用者執行緒一起)

基於標記結果,直接清理物件

CMS收集器看著很完美,但是也存在一些缺點:

  • 系統的吞吐量會下降,因為要拿出一部分cpu資源去進行垃圾收集
  • 清理不徹底,因為使用者執行緒和GC收集執行緒是同步進行的雖然有兩次標記的過程,但還是會產生新的垃圾

總結

本文分別介紹了垃圾識別演算法、垃圾收集演算法、常用的垃圾收集器,理解了這些內容後,當我們再遇到OOM時可以根據heap dump中紀錄的GC情況進行分析,為什麼GC沒有成功收集到失效記憶體,從而找出記憶體洩漏的根因,這對於我們理解JVM和分析生產事故dump都有很大的幫助,同時這部分也是面試官經常問的問題,起碼我是經常會問面試者這些問題來考察面試者對GC的理解深度。

附錄:

JVM GC相關引數
-XX:+UseSerialGC:在新生代和老年代使用序列收集器

-XX:SurvivorRatio:設定eden區大小和survivior區大小的比例

-XX:NewRatio:新生代和老年代的比

-XX:+UseParNewGC:在新生代使用並行收集器

-XX:+UseParallelGC :新生代使用並行回收收集器

-XX:+UseParallelOldGC:老年代使用並行回收收集器

-XX:ParallelGCThreads:設定用於垃圾回收的執行緒數

-XX:+UseConcMarkSweepGC:新生代使用並行收集器,老年代使用CMS+序列收集器

-XX:ParallelCMSThreads:設定CMS的執行緒數量

-XX:CMSInitiatingOccupancyFraction:設定CMS收集器在老年代空間被使用多少後觸發

-XX:+UseCMSCompactAtFullCollection:設定CMS收集器在完成垃圾收集後是否要進行一次記憶體碎片的整理

-XX:CMSFullGCsBeforeCompaction:設定進行多少次CMS垃圾回收後,進行一次記憶體壓縮

-XX:+CMSClassUnloadingEnabled:允許對類後設資料進行回收

-XX:CMSInitiatingPermOccupancyFraction:當永久區佔用率達到這一百分比時,啟動CMS回收

-XX:UseCMSInitiatingOccupancyOnly:表示只在到達閥值的時候,才進行CMS回收

相關文章