《玩轉JVM之垃圾收集器》
垃圾回收器分類方式
- 串行垃圾回收器(Serial)or并行垃圾回收器。
- 新生代垃圾回收器or老年代垃圾回收器
-
單程序垃圾回收器or多線程垃圾回收器
我們這裡按串行和并行垃圾回收器進行介紹,主要講述G1和CMS面試常問垃圾回收處理器。

串行垃圾回收器-Serial
串行垃圾回收器特點:
- 隻使用單線程進行GC(CPU隻有一個,單線程的效率比較高,因為CPU比較專注。多線程得考慮頁面切換問題)
-
獨占式的GC(STW,Stop-The-World,為了擷取停止時的快照,此時所有線程都會停止)
串行收集器是JVM Client模式下預設的垃圾收集器。
串行垃圾回收器回收示意圖:單線程化
JVM參數調整:
啟用指定垃圾收集器:
針對各個時期采用的算法:
并行垃圾回收器-ParNew & ParallelGC& ParallelOldGc
将串行回收器多線程化,與串行回收器有相同的回收政策、算法、參數。(Parallel:水準的意思)
并行垃圾回收器回收示意圖:多線程
啟用指定垃圾收集器:
對各個時期采用的算法:
并行回收器-CMS:
CMS垃圾回收步驟,共6步(有的也說7步,多了一步在并發預清理上):
- 初始标記(Initial Mark):從Roots開始标記根對象。(STW會導緻所有的線程被停止)【關鍵點】
- 主要标記過程
- 從GC Roots周遊可直達的老年代對象;
- 周遊被新生代存活對象所引用的老年代對象。
- 程式執行情況
- 支援單線程或并行标記。
- 發生stop-the-world,暫停所有應用線程。
- 主要标記過程
- 并發标記(Concurrent Mark):在該階段,GC線程和應用線程将并發執行。也就是說,在第一個階段(Initial Mark)被暫停的應用線程将恢複運作。主要工作:通過周遊第一個階段的存活對象,标記老年代中剩下的所有對象。
- 由于在并發标記階段,應用線程和GC線程是并發執行的,是以可能産生新的對象或對象關系發生變化
- 老年代對象的引用關系發生變更;
- 直接在老年代配置設定對象;
- 新生代的對象晉升到老年代;等
對于這些對象,需要重新标記以防止被遺漏。為了提高重新标記的效率,本階段會把這些發生變化的對象所在的Card辨別為Dirty,這樣後續就隻需要掃描這些Dirty Card的對象,進而避免掃描整個老年代。
并發預清理(Concurrent Preclean):在并發預清理階段,将會重新掃描标記的Dirty對象,并标記被Dirty對象直接或間接引用的對象,然後清除Card辨別。标記被Dirty對象直接或間接引用的對象,清除Dirty對象的Card辨別。(有的文章寫7個階段,多出來的那一階段在這裡。)
- 由于在并發标記階段,應用線程和GC線程是并發執行的,是以可能産生新的對象或對象關系發生變化
- 可中止的并發預清理(Concurrent Abortable Preclean):本階段盡可能承擔更多的并發預處理工作,進而減輕在Final Remark階段的stop-the-world。
- 在該階段,主要循環的做兩件事:
- 處理 From 和 To 區的對象,标記可達的老年代對象;
- 和上一個階段一樣,掃描處理Dirty Card中的對象。
- 具體執行多久,取決于許多因素,滿足其中一個條件将會中止運作:
- 執行循環次數達到了門檻值;
- 執行時間達到了門檻值;
- 新生代Eden區的記憶體使用率達到了門檻值。
- 在該階段,主要循環的做兩件事:
- 重新标記(Final Remark):預清理階段也是并發執行的,并不一定是所有存活對象都會被标記,因為在并發标記的過程中對象及其引用關系還在不斷變化中。是以,需要有一個stop-the-world的階段來完成最後的标記工作,這就是重新标記階段(CMS标記階段的最後一個階段)。主要目的是重新掃描之前并發處理階段的所有殘留更新對象。
- 主要工作:
- 周遊新生代對象,重新标記;(新生代會被分塊,多線程掃描)
- 根據GC Roots,重新标記;
- 周遊老年代的Dirty Card,重新标記。這裡的Dirty Card,大部分已經在預清理階段被處理過了。
- 主要工作:
- 并發清理(Concurrent Sweep):并發清理階段,主要工作是清理所有未被标記的死亡對象,回收被占用的空間。【關鍵點】
- 并發重置(Concurrent Reset):将清理并恢複在CMS GC過程中的各種狀态,重新初始化CMS相關資料結構,為下一個垃圾收集周期做好準備。
總結:在并發清理前,我們所有的工作都是為了标記存活對象,清理死亡對象隻有在并發清理進行。
收集器JVM參數:
G1垃圾回收器
G1全稱Garbage First Garbage Collector,優先回收垃圾比例最高的區域(分區算法)。G1收集器将堆劃分為多個區域,每次收集部分區域來減少GC産生的停頓時間。JDK1,7引入,不同于其他垃圾回收處理器,他是直接負責全代(包括新生代和老年代)。
GC收集過程
- 第一階段:新生代GC
- 第二階段:并發标記周期
- 第三階段:混合收集。
- 第四階段:Full GC(非必須)
G1第一階段:新生代GC
- 當Eden區被占滿,新生代GC就會被啟動。
- 回收後,Edenl區會被清空,Survio會保留一部分資料。
- 部分新生代對象會普升到老年代。
G1第二階段:并發标記周期
看過程會發現很眼熟,這是因為G1是在CMS的基礎上改進而來,在JDK1.7的時候引入的,下面來說一下經曆的六個過程:
初始标記(Initial Mark):從Roots開始标記根對象。(STW會導緻所有的線程被停止)【關鍵點】
- 主要标記過程
- 從GC Roots周遊可直達的老年代對象;
- 周遊被新生代存活對象所引用的老年代對象。
初始标記(Initial Mark)負責标記所有能被直接可達的根對象(原生棧對象、全局對象、JNI對象),
根是對象圖的起點,是以初始标記需要将Mutator線程(Java應用線程)暫停掉,也就是需要一個STW的時間段。
事實上,當達到IHOP門檻值時,G1并不會立即發起并發标記周期,而是等待下一次年輕代收集,
利用年輕代收集的STW時間段,完成初始标記,這種方式稱為借道(Piggybacking)。在初始标記暫停中,
分區的NTAMS都被設定到分區頂部Top,初始标記是并發執行,直到所有的分區處理完。
這個階段就是從根結點周遊出可直達的老年對象。
根區域掃描(Root Region Scanning):因為先進行了一次 YGC,是以目前年輕代隻有 Survivor 區有存活對象,它被稱為根引用區。掃描 Survivor 到老年代的引用,該階段必須在下一次 Young GC 發生前結束。下面是官方文檔的中文翻譯:
在初始标記暫停結束後,年輕代收集也完成的對象複制到Survivor的工作,應用線程開始活躍起來。
此時為了保證标記算法的正确性,所有新複制到Survivor分區的對象,都需要被掃描并标記成根,
這個過程稱為根分區掃描(Root Region Scanning),同時掃描的Suvivor分區也被稱為根分區(Root Region)。
根分區掃描必須在下一次年輕代垃圾收集啟動前完成(并發标記的過程中,可能會被若幹次年輕代垃圾收集打斷),
因為每次GC會産生新的存活對象集合。
PS:這個階段不能發生年輕代收集,如果中途 Eden 區真的滿了,也要等待這個階段結束才能進行 Young GC。
這個階段做的一件事就是掃描并且标記Survivor區直接能夠到達老年代的對象。
并發标記(Concurrent Mark):在該階段,GC線程和應用線程将并發執行。也就是說,在第一個階段(Initial Mark)被暫停的應用線程将恢複運作。主要工作:通過周遊第一個階段的存活對象,标記老年代中剩下的所有對象。
- 由于在并發标記階段,應用線程和GC線程是并發執行的,是以可能産生新的對象或對象關系發生變化
- 老年代對象的引用關系發生變更;
- 直接在老年代配置設定對象;
- 新生代的對象晉升到老年代;等
和應用線程并發執行,并發标記線程在并發标記階段啟動,由參數-XX:ConcGCThreads(預設GC線程數的1/4,
即-XX:ParallelGCThreads/4)控制啟動數量,每個線程每次隻掃描一個分區,進而标記出存活對象圖。
在這一階段會處理Previous/Next标記位圖,掃描标記對象的引用字段。
同時,并發标記線程還會定期檢查和處理STAB全局緩沖區清單的記錄,更新對象引用資訊。
參數-XX:+ClassUnloadingWithConcurrentMark會開啟一個優化,如果一個類不可達(不是對象不可達),
則在重新标記階段,這個類就會被直接解除安裝。所有的标記任務必須在堆滿前就完成掃描,如果并發标記耗時很長,
那麼有可能在并發标記過程中,又經曆了幾次年輕代收集。如果堆滿前沒有完成标記任務,則會觸發擔保機制,
經曆一次長時間的串行Full GC。
PS:與CMS不同的是這個階段是并發執行的,中間可以發生多次 Young GC,Young GC 會中斷标記過程。
這個階段做的一件事就是循環周遊第一個階段的存活對象,标記整個堆剩下的所有存活對象。
重新标記(Final Remark):先stop-the-world,完成最後的存活對象标記。使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法。
- 主要工作:
- 周遊新生代對象,重新标記;(新生代會被分塊,多線程掃描)
- 根據GC Roots,重新标記;
- 周遊老年代的Dirty Card,重新标記。這裡的Dirty Card,大部分已經在預清理階段被處理過了。
重新标記(Remark)是最後一個标記階段。在該階段中,G1需要一個暫停的時間,
去處理剩下的SATB日志緩沖區和所有更新,找出所有未被通路的存活對象,同時安全完成存活資料計算。
這個階段也是并行執行的,通過參數-XX:ParallelGCThread可設定GC暫停時可用的GC線程數。
同時,引用處理也是重新标記階段的一部分,
所有重度使用引用對象(弱引用、軟引用、虛引用、最終引用)的應用都會在引用處理上産生開銷。
獨占清理(cleanup):計算各個區域的存活對象和GC回收比例,并進行排序,識别可以混合回收的區域。為下階段做鋪墊。是STW的。(類似于分區算法,将區域分為一個個小區塊,根據存活對象和死亡對象比例進行回收)
并發清理(Concurrent Sweep):識别并清理完全空閑的區域。
G1第三階段:混合收集
G1收集過程:因為Full GC是非必須的,是以G1收集過程一直都是這三個階段重複運作。
混合收集(Mixed GC):當越來越多的對象晉升到老年代old region時,為了避免堆記憶體被耗盡,虛拟機會觸發一個混合的垃圾收集器,即Mixed GC,該算法并不是一個Old GC,除了回收整個Young Region,還會回收一部分的Old Region。這裡需要注意: 是一部分老年代,而不是全部老年代。可以選擇哪些Old Region進行收集,進而可以對垃圾回收的耗時時間進行控制。也要注意的是Mixed GC并不是Full GC。
- 并發标記結束以後,老年代中百分百為垃圾的記憶體分段被回收了,部分為垃圾的記憶體分段被計算了出來。預設情況下,這些老年代的記憶體分段會分8次(可以通過-XX:G1MixedGCCountTarget設定)被回收。
- 混合回收的回收集(Collection Set)包括八分之一的老年代記憶體分段,Eden區記憶體分段,Survivor區記憶體分段。混合回收的算法和年輕代回收的算法完全一樣,隻是回收集多了老年代的記憶體分段。具體過程請參考上面的年輕代回收過程。
- 由于老年代中的記憶體分段預設分8次回收,G1會優先回收垃圾多的記憶體分段。
。并且有一個門檻值會決定記憶體分段是否被回收。-XX:G1MixedGCLiveThresholdPercent,預設為65%,意思是垃圾占記憶體分段比例要達到65%才會被回收。如果垃圾占比太低,意味着存活的對象占比高,在複制的時候會花費更多的時間。垃圾占記憶體分段比例越高,越會被先回收
- 混合回收并不一定要進行8次。有一個門檻值-XX:G1HeapWastePercent,預設值為10%,意思是允許整個堆記憶體中有10%的空間被浪費,意味着如果發現可以回收的垃圾占堆記憶體的比例低于10%,則不再進行混合回收。因為GC會花費很多的時間但是回收到的記憶體卻很少。
JVM參數:
第四階段:Full GC(非必須)
G1的初衷就是要避免Full GC的出現。按時如果上述方式不能正常工作,G1會
停止應用程式的執行
(Stop-The-World),使用單線程的記憶體回收算法進行垃圾回收,性能會非常差,應用程式停頓時間會很長。
要避免Full GC的發生,一旦發生需要進行調整。什麼時候會發生Full GC呢?比如
堆記憶體太小
,當G1在複制存活對象的時候沒有空的記憶體分段可用,則會回退到full gc,這種情況可以通過增大記憶體解決,可能原因如下:
- Evacuation的時候沒有足夠的to-space來存放晉升的對象;
-
并發處理過程完成之前空間耗盡。
文章最後再給大家送上JVM常用調優參數:
至此,垃圾回收器就講完了,如果文章還有什麼不足之處,請各位讀者進行回報,我會根據回報對文章進行修改和完善。