天天看點

《玩轉JVM之垃圾收集器》

《玩轉JVM之垃圾收集器》

垃圾回收器分類方式

  • 串行垃圾回收器(Serial)or并行垃圾回收器。
  • 新生代垃圾回收器or老年代垃圾回收器
  • 單程序垃圾回收器or多線程垃圾回收器

    我們這裡按串行和并行垃圾回收器進行介紹,主要講述G1和CMS面試常問垃圾回收處理器。

《玩轉JVM之垃圾收集器》

串行垃圾回收器-Serial

串行垃圾回收器特點:
  • 隻使用單線程進行GC(CPU隻有一個,單線程的效率比較高,因為CPU比較專注。多線程得考慮頁面切換問題)
  • 獨占式的GC(STW,Stop-The-World,為了擷取停止時的快照,此時所有線程都會停止)

    串行收集器是JVM Client模式下預設的垃圾收集器。

串行垃圾回收器回收示意圖:單線程化
《玩轉JVM之垃圾收集器》
JVM參數調整:
《玩轉JVM之垃圾收集器》
啟用指定垃圾收集器:
《玩轉JVM之垃圾收集器》
針對各個時期采用的算法:
《玩轉JVM之垃圾收集器》

并行垃圾回收器-ParNew & ParallelGC& ParallelOldGc

将串行回收器多線程化,與串行回收器有相同的回收政策、算法、參數。(Parallel:水準的意思)

并行垃圾回收器回收示意圖:多線程
《玩轉JVM之垃圾收集器》
啟用指定垃圾收集器:
《玩轉JVM之垃圾收集器》
對各個時期采用的算法:
《玩轉JVM之垃圾收集器》

并行回收器-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個階段,多出來的那一階段在這裡。)

  • 可中止的并發預清理(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之垃圾收集器》
收集器JVM參數:
《玩轉JVM之垃圾收集器》

G1垃圾回收器

G1全稱Garbage First Garbage Collector,優先回收垃圾比例最高的區域(分區算法)。G1收集器将堆劃分為多個區域,每次收集部分區域來減少GC産生的停頓時間。JDK1,7引入,不同于其他垃圾回收處理器,他是直接負責全代(包括新生代和老年代)。

GC收集過程
  • 第一階段:新生代GC
  • 第二階段:并發标記周期
  • 第三階段:混合收集。
  • 第四階段:Full GC(非必須)

G1第一階段:新生代GC

  • 當Eden區被占滿,新生代GC就會被啟動。
  • 回收後,Edenl區會被清空,Survio會保留一部分資料。
  • 部分新生代對象會普升到老年代。
《玩轉JVM之垃圾收集器》

G1第二階段:并發标記周期

看過程會發現很眼熟,這是因為G1是在CMS的基礎上改進而來,在JDK1.7的時候引入的,下面來說一下經曆的六個過程:

初始标記(Initial Mark):從Roots開始标記根對象。(STW會導緻所有的線程被停止)【關鍵點】

  • 主要标記過程
    • 從GC Roots周遊可直達的老年代對象;
    • 周遊被新生代存活對象所引用的老年代對象。
    事實上G1并不會立即發起并發标記周期,而是等待下一次年輕代收集,利用年輕代收集的STW時間段,完成初始标記,這種方式稱為借道(Piggybacking),然後對 Survivor 區(root region)進行标記,因為該區可能存在對老年代的引用,下面是官方文檔的中文翻譯:
初始标記(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區直接能夠到達老年代的對象。

《玩轉JVM之垃圾收集器》

并發标記(Concurrent Mark):在該階段,GC線程和應用線程将并發執行。也就是說,在第一個階段(Initial Mark)被暫停的應用線程将恢複運作。主要工作:通過周遊第一個階段的存活對象,标記老年代中剩下的所有對象。

  • 由于在并發标記階段,應用線程和GC線程是并發執行的,是以可能産生新的對象或對象關系發生變化
    • 老年代對象的引用關系發生變更;
    • 直接在老年代配置設定對象;
    • 新生代的對象晉升到老年代;等
    對于這些對象,需要重新标記以防止被遺漏。為了提高重新标記的效率,本階段會把這些發生變化的對象所在的Card辨別為Dirty,這樣後續就隻需要掃描這些Dirty Card的對象,進而避免掃描整個老年代。
和應用線程并發執行,并發标記線程在并發标記階段啟動,由參數-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收集過程一直都是這三個階段重複運作。

《玩轉JVM之垃圾收集器》

混合收集(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之垃圾收集器》

JVM參數:

《玩轉JVM之垃圾收集器》

第四階段:Full GC(非必須)

G1的初衷就是要避免Full GC的出現。按時如果上述方式不能正常工作,G1會

停止應用程式的執行

(Stop-The-World),使用單線程的記憶體回收算法進行垃圾回收,性能會非常差,應用程式停頓時間會很長。

要避免Full GC的發生,一旦發生需要進行調整。什麼時候會發生Full GC呢?比如

堆記憶體太小

,當G1在複制存活對象的時候沒有空的記憶體分段可用,則會回退到full gc,這種情況可以通過增大記憶體解決,可能原因如下:

  • Evacuation的時候沒有足夠的to-space來存放晉升的對象;
  • 并發處理過程完成之前空間耗盡。

    文章最後再給大家送上JVM常用調優參數:

《玩轉JVM之垃圾收集器》
《玩轉JVM之垃圾收集器》

至此,垃圾回收器就講完了,如果文章還有什麼不足之處,請各位讀者進行回報,我會根據回報對文章進行修改和完善。