Shenandoah GC:一個來自OpenJDK12的全新併發壓縮垃圾回收器

是不是才聽說了JDK11的ZGC,並且還沒搞懂?不好意思,OpenJDK12馬不停蹄的帶來了Shenandoah GC

概述

JDK12新增的一個名爲Shenandoah的GC算法,它的evacuation階段工作能通過與正在運行中Java工作線程同時進行(即併發,concurrent),從而減少GC的停頓時間。

Shenandoah的停頓時間和堆的大小沒有任何關係,這就意味着無論你的堆是200MB,2GB還是200GB,停頓時間是一樣的。

shenandoah GC Phase

如上圖所示,Shenandoah GC每個GC週期由2個STW(Stop The World)階段和2個併發階段組成。在初始化標記階段,掃描root集合的時候會STW。然後併發標記階段,Shenandoah GC和Java工作線程一起運行,最後,在最終標記階段,又會STW,然後執行一個併發evacuation階段。

Root集合包括:thread local variables, references embedded in generated code, interned Strings, references from classloaders (e.g. static final references), JNI references, JVMTI references.

深入剖析

Shenandoah是一個基於Region設計的垃圾收集器,這點和G1類似,它把整個堆當作Region集合來維護。但是,Shenandoah不需要remember set或者card table來記錄跨region引用。 
其中一個原因是(無條件)card mark可能引起false sharing,Brooks pointer分散在每個對象頭上,比較不容易引起false sharing:

Shenandoah Brooks Pointer

一個常規的Shenandoah GC週期大概是這樣的(跟G1也有點相似):

Shenandoah GC Cycle

GC日誌如下:

GC(3) Pause Init Mark 0.771ms
GC(3) Concurrent marking 76480M->77212M(102400M) 633.213ms
GC(3) Pause Final Mark 1.821ms
GC(3) Concurrent cleanup 77224M->66592M(102400M) 3.112ms
GC(3) Concurrent evacuation 66592M->75640M(102400M) 405.312ms
GC(3) Pause Init Update Refs 0.084ms
GC(3) Concurrent update references  75700M->76424M(102400M) 354.341ms
GC(3) Pause Final Update Refs 0.409ms
GC(3) Concurrent cleanup 76244M->56620M(102400M) 12.242ms

每個階段要做的事情如下:

Init Mark 併發標記的初始化階段,它爲併發標記準備堆和應用線程,然後掃描root集合。這是整個GC生命週期第一次停頓,這個階段主要工作是root集合掃描,所以停頓時間主要取決於root集合大小。

Concurrent Marking 貫穿整個堆,以root集合爲起點,跟蹤可達的所有對象。 這個階段和應用程序一起運行,即併發(concurrent)。這個階段的持續時間主要取決於存活對象的數量,以及堆中對象圖的結構。由於這個階段,應用依然可以分配新的數據,所以在併發標記階段,堆佔用率會上升。

Final Mark 清空所有待處理的標記/更新隊列,重新掃描root集合,結束併發標記。. 這個階段還會搞明白需要被清理(evacuated)的region(即垃圾收集集合),並且通常爲下一階段做準備。最終標記是整個GC週期的第二個停頓階段,這個階段的部分工作能在併發預清理階段完成,這個階段最耗時的還是清空隊列和掃描root集合。

Concurrent Cleanup 回收即時垃圾區域 -- 這些區域是指併發標記後,探測不到任何存活的對象。

Concurrent Evacuation 從垃圾收集集合中拷貝存活的對到其他的region中,這是有別於OpenJDK其他GC主要的不同點。這個階段能再次和應用一起運行,所以應用依然可以繼續分配內存,這個階段持續時間主要取決於選中的垃圾收集集合大小(比如整個堆劃分128個region,如果有16個region被選中,其耗時肯定超過8個region被選中)。

Init Update Refs 初始化更新引用階段,它除了確保所有GC線程和應用線程已經完成併發Evacuation階段,以及爲下一階段GC做準備以外,其他什麼都沒有做。這是整個GC週期中,第三次停頓,也是時間最短的一次。

Concurrent Update References 再次遍歷整個堆,更新那些在併發evacuation階段被移動的對象的引用。這也是有別於OpenJDK其他GC主要的不同,這個階段持續時間主要取決於堆中對象的數量,和對象圖結構無關,因爲這個過程是線性掃描堆。這個階段是和應用一起併發運行的。

Final Update Refs 通過再次更新現有的root集合完成更新引用階段,它也會回收收集集合中的region,因爲現在的堆已經沒有對這些region中的對象的引用。

這是整個GC週期最後一個階段,它的持續時間主要取決於root集合的大小。

Concurrent Cleanup 回收那些現在沒有任何引用的Region集合。

目標

Shenandoah不是一個要一統天下的GC,有一些其他的吞吐量優先,或者內存佔用優先的GC算法,它們並不是把響應性放在第一位(即不是主要考慮縮短停頓時間)。

Shenandoah是一個對那些更看重響應性和可預測短暫停頓的應用來說,更合適的GC算法。它的目標不是要解決所有JVM的停頓問題,由於GC之外的其他原因(例如到達安全點時間(TTSP--Time To Safe Point)問題)而暫停時間超出了此JEP的範圍。

現代服務器比以前擁有更多的內存和處理器,SLA應用需要保證RP在10~500ms。爲了達到
最苛刻的目標(保證RP在10ms以內),我們需要GC的算法足夠高效,允許程序在可用內存中運行,並且經過優化後,永遠不會讓正在運行的程序的停頓時間超過5毫秒(a handful of milliseconds,一隻手就5根手指頭,所以是5ms)。

Shenandoah就是這樣一個OpenJDK爲更近這個目標而設計的開源、低停頓時間的垃圾回收器。

替代方案

1. Zing/Azul是一個沒有停頓的垃圾收集器,但是不會貢獻給OpenJDK。

2. 基於colored pointers設計的ZGC也是一個擁有很低停頓時間的垃圾收集器,Shenandoah期望能與之一戰。

3. G1很多工作都是並行或者併發的,但是evacuation階段不能併發執行。

4. CMS能併發標記,但是它執行年輕代拷貝時,需要STW,並且不會壓縮老年代,這就會導致花費更多時間來管理老年代中的可用空間以及碎片問題。

使用

這還是一個體驗功能,需要增加-XX:+UnlockExperimentalVMOptions參數才能開啓Shenandoah GC:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

常規測試

RedHat已經做了大量的測試,OpenJDK也爲Shenandoah開發了很多測試用例。而且從Fedora 24開始Shenandoah在Fedora中隨着JDK一起發佈,並在Rhel7.4中作爲技術預覽. 通過-XX:+UseShenandoahGC運行標準的OpenJDK完全足夠。

壓力測試

關於CMS,G1,ParallelOld,Shenandoah的延遲測試對比,如下圖所示:

workload

參考鏈接

  • 參考[1]:https://openjdk.java.net/jeps/189

  • 參考[2]:https://wiki.openjdk.java.net/display/shenandoah/Main

  • 參考[3]:https://hllvm-group.iteye.com/group/topic/39407

  • 參考[4]: https://www.researchgate.net/publication/306112816_Shenandoah_An_open-source_concurrent_compacting_garbage_collector_for_OpenJDK

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章