JDK14-ZGC調研初探

zhao發表於2020-08-22

原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

背景

公司ElasticSearch準備進行升級,而ElasticSearch7以上則是已經在支援使用JDK11了,JDK11中最大的特點就是 ZGC,更快的垃圾回收,更爽的快感,你懂的;所以,調研zgc的特性以及使用方式就迫在眉睫,再加上jdk14也已經剛出不久,所以則是直接以JDK14為基礎,進行了相關的測試和參考了相關的文獻後,寫了該文章,當前文章最初是2020-6月份就已經寫好的一篇文章,然後團隊內部做了彙報後就一直停留在我的筆記中了,今天閒來無事,釋出到部落格上,供大家需要時參考時使用。

前言

Open JDK11引入了ZGC的垃圾收集器,而在JDK12中引入了 Shenandoah 收集器:

背景:在《深入理解JAVA虛擬機器》文章中有提到,Shenandoah更像是一個原有的G1收集器的升級版本,且由於該收集器是來自於外部團隊所進行開發的,所以Oracle自身的JDK版本是已經對外宣佈不支援該Shenandoah收集器,轉而重推自己所開發的ZGC收集器;所以關於Shenandoah收集器則全面遷移到了OpenJDK上,這也是少見的免費版的JDK功能,多於商業版OracleJDK功能的一次;

由於ZGC是Oracle官方主推的GC收集器,且ZGC收集器的特性與Shenandoah實現過於相似,所以此處關於JDK14GC收集器的使用,則直接以ZGC為基礎進行相關調研;

建議在瞭解ZGC收集器之前,先來了解下CMS與G1收集器?為什麼?

因為:CMS的併發標記的四個階段 與G1的回收階段以及ZGC的回收階段過程全部相同,所有的物件清理階段都是 初始標記,併發標記,最終標記 & 垃圾清除;而G1與ZGC這些後續升級的收集器最大的變動,其實也就是優化上述的四個階段;比如 G1優化了CMS最終標記階段為了解決跨代引用而導致的全堆掃描問題,而ZGC在G1的基礎上則更加徹底的優化了上述的回收過程;當然為了優化上述的過程,從記憶體結構的分配到一些演算法的實現改動是很大的,但目的是一致的,減少標記時間,減少清理時間;

CMS的收集器是JDK1.8之前的經典收集器,JDK1.8以後大力推廣G1的收集器效果,而在JDK14中則完全拋棄了CMS收集器,轉而重推號稱
無論執行在任何量級的堆上都可以達到最大GC停頓時間不會超過10ms的ZGC收集器,那麼為什麼ZGC可以做到如此優異的回收效果?ZGC的演變歷史是什麼?以及後續我們如何在Java程式如何使用ZGC?如何在服務端除錯以及優化ZGC?帶著這些問題向下看即可;

注意:CMS是標記清除演算法,而G1與ZGC則是標記整理演算法,所以G1和ZGC也不會存在CMS的碎片問題;

JVM引數使用

JVM除錯相關

檢視當前JVM的預設收集器:java -XX:+PrintCommandLineFlags -version

檢視當前程式的Heap概要資訊,GC使用的演算法等資訊(JDK14中不再支援jmap的-heap引數): jmap -heap PID

輸出當前JVM程式的所有JVM引數的值:jinfo -flags PID

JDK GC配置比對

配置當前JDK8的服務開啟G1收集器

-Xmx108M
-Xms108M
-XX:MaxMetaspaceSize=112M
-XX:MetaspaceSize=112M
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+ParallelRefProcEnabled
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-Xloggc:C:\Arnold\workSpace\GC\gc.log

配置當前JDK14的服務開啟ZGC收集器

-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
-Xmx100M
-Xlog:gc*:C:\Arnold\workSpace\GC\jdk14gc.log

ZGC收集器已經不再推薦之前老的日誌配置方式,比如:-XX:+PrintGCDetails,-Xloggc: 等等,這些老的引數
已經不再推薦使用,並且部分引數已經不在支援了,後續關於gcLog的配置統一使用一個引數即可:-Xlog:gc: ;
JDK警告如下:
[0.051s][warning][gc] -Xloggc is deprecated. Will use -Xlog:gc:C:\Arnold\workSpace\GC\jdk14gc.log instead.
[0.052s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.

-Xlog:gc: 此處的 表示輸出當前gc日誌的詳細資訊,如果是直接配置:-Xlog:gc: 輸出的日誌結果將是非明細版;

jinfo對比

使用Jinfo檢視當前JDK8的預設程式所有JVM引數如下:

C:\Program Files\Java\jdk1.8.0_201\bin>jinfo -flags 55888
VM flags: -XX:CICompilerCount=3 -XX:CompressedClassSpaceSize=109051904 -XX:ConcGCThreads=1 -XX:G1HeapRegionSize=1048576 -XX:InitialHeapSize=113246208 -XX:MarkStackSize=4194304 -XX:MaxGCPauseMillis=100 -XX:MaxHeapSize=113246208 -XX:MaxMetaspaceSize=117440512 -XX:MaxNewSize=67108864 -XX:MetaspaceSize=117440512 -XX:MinHeapDeltaBytes=1048576 -XX:+ParallelRefProcEnabled -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
Command line:  -Xmx108M -Xms108M -XX:MaxMetaspaceSize=112M -XX:MetaspaceSize=112M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.3.2\lib\idea_rt.jar=57080:C:\Program Files\JetBrains\IntelliJ IDEA 2018.3.2\bin -Dfile.encoding=UTF-8

JDK14程式所對應的JVM引數如下:

C:\Apps\openjdk-14.0.1_windows-x64_bin\jdk-14.0.1\bin>jinfo -flags 12268
VM Flags:
-XX:CICompilerCount=3 -XX:InitialHeapSize=65011712 -XX:MaxHeapSize=104857600 -XX:MinHeapDeltaBytes=2097152 -XX:MinHeapSize=8388608 -XX:NonNMethodCodeHeapSize=5832780 -XX:NonProfiledCodeHeapSize=122912730 -XX:ProfiledCodeHeapSize=122912730 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:SoftMaxHeapSize=104857600 -XX:+UnlockExperimentalVMOptions -XX:-UseCompressedClassPointers -XX:-UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseZG

Jstat對比各收集器執行資訊及記憶體分佈資訊

JDK8 G1如下:

C:\Program Files\Java\jdk1.8.0_201\bin>jstat -gc 55888 1000 10
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
 0.0    0.0    0.0    0.0    6144.0   2048.0   104448.0     0.0     4480.0 775.8  384.0   76.4       0    0.000   0      0.000    0.000
 0.0    0.0    0.0    0.0    6144.0   2048.0   104448.0     0.0     4480.0 775.8  384.0   76.4       0    0.000   0      0.000    0.000

JDK14 ZGC如下:

C:\Apps\openjdk-14.0.1_windows-x64_bin\jdk-14.0.1\bin>jstat -gc 12268 1000 10
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT
  -      -      -      -       -        -       8192.0      0.0      0.0    0.0    0.0    0.0        -        -   -          -   0      0.000    0.000
  -      -      -      -       -        -       8192.0      0.0      0.0    0.0    0.0    0.0        -        -   -          -   0      0.000    0.000

根據上述ZGC輸出可知:
ZGC已經不再存在:S0C,S1C,S0U,S1U,EC,EU 這些概念,且也已經不再存在YGC,FGC這些概念,轉而變更為了CGC;
所以,目前版本的ZGC是沒有分代收集的概念的,當然不排除後續是否會新增分代的概念進去,但目前是不存在分代收集的,
詳情看下方關於ZGC的詳細介紹;
原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

GC日誌比對

通過窺看GC的Log日誌,基本就可以大概瞭解當前的gc收集器的一些特點和原理,此處只擷取一些重要的資訊:

G1

ZGC

ZGC init

ZGC初始化資訊,可以看到有提示:NUMA Support: Disabled,Large Page Support: Disabled,還有當前執行的執行緒併發執行緒數,Min Capacity: 8M 初始化容量,Pre-touch: Disabled等資訊,而這些資訊都是在ZGC中相對比較重要的概念;

[0.040s][info][gc,init] Initializing The Z Garbage Collector
[0.041s][info][gc,init] Version: 14.0.1+7 (release)
[0.041s][info][gc,init] NUMA Support: Disabled
[0.041s][info][gc,init] CPUs: 4 total, 4 available
[0.042s][info][gc,init] Memory: 3892M
[0.042s][info][gc,init] Large Page Support: Disabled
[0.042s][info][gc,init] Medium Page Size: N/A
[0.042s][info][gc,init] Workers: 1 parallel, 1 concurrent
[0.044s][info][gc,init] Address Space Type: Contiguous/Unrestricted/Complete
[0.044s][info][gc,init] Address Space Size: 1600M x 3 = 4800M
[0.044s][info][gc,init] Min Capacity: 8M
[0.044s][info][gc,init] Initial Capacity: 62M
[0.044s][info][gc,init] Max Capacity: 100M
[0.044s][info][gc,init] Max Reserve: 2M
[0.044s][info][gc,init] Pre-touch: Disabled
[0.046s][info][gc,init] Uncommit: Enabled, Delay: 300s
[0.063s][info][gc,init] Runtime Workers: 1 parallel

好訊息是:在ZGC的JVM引數中,也就只有這麼幾個引數是很重要的概念,ZGC的JVM引數並不複雜,所以可調優的空間以及可調優的引數也相對不多;

PS:感覺後續未來JDK的GC發展,也越來越是向簡單實用的方向發展,你只需要簡單的配置幾個JVM引數以後,JVM自身內部就會做很多的處理,且效能極高;
並且不會再像CMS等GC那樣需要有較為繁瑣的配置,既要關注配置多大的比例觸發CMS才能保證回收效率和空間使用的雙向合理,又要關注碎片整理等問題;
而在未來,我覺得,未來的GC配置基本上是可以做到不用調優的,因為所有的調優操作,JVM內部都已經幫你做過了;甚至於說為什麼未來的GC不需要再手動的過分調優?因為1、GC內部已經會做自適應調優,2、JDK就已經不會再對外拋給你這麼多可調有的引數給你用了,也就是你想調優就基本也沒有什麼可調優空間了;
當然不調優不意味著就不需要再瞭解它了,因為至少目前來看,一些併發執行緒的數量配置,記憶體的大小,還是要開發者自己關注並適配的,如果真的GC回收很慢怎麼辦,那你還是要手動調優的。至少要做到,通過對ZGC的實現原理的瞭解 + 檢視相關的ZGC的日誌,要能夠定位到,是什麼原因導致的很慢?記憶體太少?資料太多?併發執行緒數太少?等等,只有定位到相關的問題後,才能有依據的修改對應的引數進行排查和優化

ZGC >>> Phases,Ref,heap

注意:我這裡是直接拿的第一次垃圾回收的完整片段Copy過來的,所以可以看到下面的GC日誌都是GC1,而對於GC0,和後續的所有GC日誌,其實本質上都是相同的回收階段和格式,所以直接看這一個片段即可;

1、開始GC回收Start
[1.432s][info][gc,start    ] GC(1) Garbage Collection (Allocation Stall)
2、清除所有的物件軟引用連結(這裡實際上以及是涉及到ZGC的實現原理了,也就是後續下面一個標題ZGC的特點實現上那些比較概念化的東西,而這裡的日誌實際上就是最好的概念化的具體體現)
[1.433s][info][gc,ref      ] GC(1) Clearing All SoftReferences
3、具體的回收階段:
3.1.1、初始化標記階段、與CMS和G1的初始化標記相同(就是標記我們GC ROOT所能引用到的物件),但是請注意,此處可以可以看到是:Pause Mark Start 也就說此處的初始化標記是STW的,這說明ZGC也並沒有完全做到真正的併發執行,完全無使用者執行緒停頓這樣一個效果,但是可以看到,儘管是STW的,但是時間非常短暫,只有0.1毫秒,所以該STW階段的耗時,基本可以忽略不計了;
[1.433s][info][gc,phases   ] GC(1) Pause Mark Start 0.124ms
3.1.2、併發標記階段:有沒有覺得特別像CMS的回收特點?由於是併發標記所以此處並不是STW的,對使用者執行緒無影響;但耗時較長
[1.436s][info][gc,phases   ] GC(1) Concurrent Mark 3.394ms
3.1.3、初始化標記結束,也是當前已經看到的第二個STW的階段了;但此處的耗時也是極短的;
[1.436s][info][gc,phases   ] GC(1) Pause Mark End 0.018ms
3.1.4、併發的處理非強引用的關聯物件
[1.437s][info][gc,phases   ] GC(1) Concurrent Process Non-Strong References 0.417ms
3.1.5、由於ZGC所採用的管理單位是以Region為一個單位,所以此處所做的事情就是標記後續需要進行整理回收的Region集合,便於後續進行Region的整理回收;
[1.437s][info][gc,phases   ] GC(1) Concurrent Reset Relocation Set 0.002ms
[1.437s][info][gc          ] Allocation Stall (main) 4.599ms
[1.437s][info][gc          ] Allocation Stall (Monitor Ctrl-Break) 4.022ms
[1.440s][info][gc,phases   ] GC(1) Concurrent Select Relocation Set 2.786ms
第三次SWT的階段:開始進行Region集合的移動
[1.441s][info][gc,phases   ] GC(1) Pause Relocate Start 0.251ms
//TODO:
[1.443s][info][gc,phases   ] GC(1) Concurrent Relocate 1.336ms
[1.444s][info][gc,load     ] GC(1) Load: 0.00/0.00/0.00
[1.444s][info][gc,mmu      ] GC(1) MMU: 2ms/73.4%, 5ms/89.3%, 10ms/94.1%, 20ms/95.2%, 50ms/98.1%, 100ms/98.7%
[1.444s][info][gc,marking  ] GC(1) Mark: 1 stripe(s), 2 proactive flush(es), 1 terminate flush(es), 0 completion(s), 0 continuation(s) 
[1.444s][info][gc,reloc    ] GC(1) Relocation: Successful, 1M relocated
[1.444s][info][gc,nmethod  ] GC(1) NMethods: 182 registered, 0 unregistered
[1.444s][info][gc,metaspace] GC(1) Metaspace: 6M used, 6M capacity, 6M committed, 8M reserved
[1.444s][info][gc,ref      ] GC(1) Soft: 29 encountered, 25 discovered, 16 enqueued
[1.444s][info][gc,ref      ] GC(1) Weak: 77 encountered, 63 discovered, 7 enqueued
[1.444s][info][gc,ref      ] GC(1) Final: 0 encountered, 0 discovered, 0 enqueued
[1.444s][info][gc,ref      ] GC(1) Phantom: 7 encountered, 5 discovered, 1 enqueued
[1.444s][info][gc,heap     ] GC(1) Min Capacity: 8M(8%)
[1.444s][info][gc,heap     ] GC(1) Max Capacity: 100M(100%)
[1.444s][info][gc,heap     ] GC(1) Soft Max Capacity: 100M(100%)
[1.444s][info][gc,heap     ] GC(1)                Mark Start          Mark End        Relocate Start      Relocate End           High               Low         
[1.444s][info][gc,heap     ] GC(1)  Capacity:      100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)        100M (100%)   
[1.444s][info][gc,heap     ] GC(1)   Reserve:        2M (2%)            2M (2%)            2M (2%)            2M (2%)            2M (2%)            2M (2%)     
[1.444s][info][gc,heap     ] GC(1)      Free:        0M (0%)            0M (0%)           86M (86%)          90M (90%)          90M (90%)           0M (0%)     
[1.444s][info][gc,heap     ] GC(1)      Used:       98M (98%)          98M (98%)          12M (12%)           8M (8%)           98M (98%)           8M (8%)     
[1.444s][info][gc,heap     ] GC(1)      Live:         -                 1M (1%)            1M (1%)            1M (1%)             -                  -          
[1.444s][info][gc,heap     ] GC(1) Allocated:         -                 0M (0%)            6M (6%)           10M (10%)            -                  -          
[1.444s][info][gc,heap     ] GC(1)   Garbage:         -                96M (97%)           6M (7%)            0M (1%)             -                  -          
[1.444s][info][gc,heap     ] GC(1) Reclaimed:         -                  -                90M (90%)          96M (96%)            -                  -          
[1.444s][info][gc          ] GC(1) Garbage Collection (Allocation Stall) 98M(98%)->8M(8%)

原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

G1特點

  1. G1採用記憶體劃分多個大小相等的Region(預設512K)來進行物件的儲存和劃分,同時每個Region被標記成E、S、O、H,分別表示Eden、Survivor、Old、Humongous。其中E、S屬於年輕代,O與H屬於老年代,H表示巨型物件,當分配的物件大於等於Region的一半時就會被認為是巨型物件

  1. Q:何時觸發年輕代GC?
    A:G1的GC回收仍然是分為兩種,年輕代GC和年老代GC,年輕代Young GC的觸發條件是:當Eden區不能夠再分配新的物件時進行觸發Young GC;觸發的動作與其它的年輕代GC收集器相同,分別是:Eden區未被回收的物件會移動到Survivor區域,同時Survivor判斷物件的晉升年齡,符合則晉升至Old;

  2. Q:何時觸發年老代GC?
    A:年老代回收在G1中被稱作為Mixed GC(混合回收),回收所有年輕代的Region + 部分年老代的 Region;Mixed GC可以通過XX:InitiatingHeapOccupancyPercent引數來設定老年代佔整個堆的比例,預設是45%,當達到這個比例時,則會觸發Mixed GC;

  3. Q:Mixed GC為什麼只會回收部分年老代?回收的判斷依據是什麼?
    A:G1中可以通過指定-XX:MaxGCPauseMillis引數來指定G1的目標停頓時間,預設是200ms;當進行年老代的回收時,G1自身會有一個停頓預測模型,它會有選擇的挑選部分Region,去儘量滿足所設定的停頓時間,所以回收部分年老代的依據是根據所對應的目標停頓時間來進行分析後回收的;

  4. Full GC:當Mixed GC的回收速度,趕不上應用程式申請記憶體的速度,此時Mixed G1就會降低到Full GC,使用Serial GC收集器進行回收;
    所以如果的確觸發了Full GC,那麼只能說明:

    1. 當前機器的記憶體的確不足以支撐現有的併發了,也就是要加記憶體了,
    2. G1的目標停頓時間設定不合理,導致每次Mixed GC為了滿足目標停頓時間的要求,每次都只能回收少量的記憶體,最終導致併發回收處理的過程中,新增物件導致記憶體空間耗盡所引發的Full GC;
    3. 調整對應的併發執行緒數量等可優化引數

G1相比CMS的提升

  1. 相比於CMS的純分代回收的概念,G1所引入的Region記憶體塊結構是一個本質的變化,基於Region來設計物件分配才能引起後續的所有變化;

  2. 跨代引用的問題:無論是CMS還是G1,都會出現新生代物件存在引用老年代物件,以及老年代物件存在引用新生代物件的問題,對於這種跨代引用的問題,CMS與G1的處理方式分別是什麼?

    • 在CMS中存在四個回收階段,分別是初始標記(STW),併發標記,重標記(STW),併發清理。由於併發標記的過程中使用者執行緒與GC執行緒是同時執行的,所以在併發標記的過程中就無法保證已經標記過的物件,在後續的使用者執行緒的操作中,是否重新進行了新的引用;也就是當前併發標記階段已經被標記為不可達的物件,可能存在被使用者執行緒重新觸發然後導致物件可達了;所以為了避免進行錯誤的物件回收,在併發標記後的重標記,則是進行最後一次可達性分析,且為了避免併發標記所會導致的使用者執行緒問題,所以重標記過程中是STW的;由於存在跨代引用的問題,所以在CMS進行重標記的時候不能只是以老年代的物件為根,判斷物件是否存在引用,因為還存在當前這個老年代物件被新生代物件引用的情況,所以CMS重標記階段唯一的做法就是掃描全堆,由於是掃描全堆進行可達性分析,且此時的重標記階段是STW的,所以此處在進行CMS回收時,將會異常耗時;(不過CMS中有提供重標記執行前先執行一下新生代的回收等操作,但儘管如此仍然不能解決全堆STW掃描而帶來的耗時問題)
    • G1採用pre-write barrier解決跨代問題。在併發標記階段,當引用關係發生變化的時候,通過pre-write barrier函式會把這種這種變化記錄並儲存在一個佇列裡,在remark階段會掃描這個佇列,通過這種方式,舊的引用所指向的物件就會被標記上,其子孫也會被遞迴標記上,這樣就不會漏標記任何物件從而解決Remark階段全堆掃描的問題;
  3. G1的停頓預測模型,根本性提升,,由於G1是採用Region記憶體塊的方式進行設計,所以在觸發Mixed GC後G1可以通過滿足只回收一部分老年代的方式,來儘可能滿足所設定的應用停頓時間,由於每次的Mixed GC的回收時間都可以控制在停頓時間之內,所以G1就很牛逼了;

ZGC特點:

ZGC為什麼可以做到比G1更快的回收效果?

  1. 實現了併發標記 &併發清理 (G1由於是隻回收部分Region且內部有自己的停頓預測模型,所以可以控制清理時的回收時間;但G1本質上在清理過程中還是並行清理的,而ZGC則做到了真正的併發清理,也就是清理過程中無需停止使用者執行緒;)
  2. 動態Region,相比於G1每個Region都是512K的特點,在ZGC中Region分為小型Region(Small Region)容量固定2MB,使用者儲存小於256K的物件,中型Region(Medium Region)容量大小為32MB,用於存放大於256KB小於4MB的物件,大型Region,容量不固定,可以動態變化,但必須是2MB的整數倍;通過採用動態Region的方式可以更好的處理大物件的分配等問題;
    3.支援Numa架構
    4.ZGC目前是沒有分代的,全堆掃描,所以也不用像G1那樣需要維護一套Remember來跟蹤跨代引用的問題(但其實理論上來說,如果ZGC實現了分代回收,那麼其效率將會更高,畢竟在初始標記等階段就不用再掃描全堆了,而其它的過程則又做到了併發處理,但也就意味著ZGC需要有一套自己更加便捷的跨代回收的方案,所以目前來看這是一種取捨了;不過官方回應是會在後續增加分代回收的功能的,只是目前還沒有完美的解決方案)

通過CMS 以及 G1可以發現,物件的回收過程一般是:初始標記,併發標記,重標記,清理 這4個階段,而G1通過停頓預測模型以及通過pre-write barrier解決跨代問題來以此優化了GC的回收效率;而ZGC則在G1之上,還解決了清理的問題,將其變更為了併發清理,這是一個很大的突破(清理過程中不再是STW的了),ZGC既然已經解決了併發標記和併發清理的問題,那麼唯一還存在STW的過程則是初始標記階段,而初始標記主要做的事情則是以Root為根物件做集合掃描:Java中Root根一般是虛擬機器棧中的引用物件,方法區中靜態屬性引用的物件,JNI中引用的物件,方法區中的引用物件;所以GC初始化標記過程中則是與Java堆中的物件數量無關的,也就是說無論我們的Java堆是有1G的大小,還是有10G的大小,對我們在初始化標記時的影響是有限的,初始化標記時的耗時頂多會和當前JVM的執行緒的多少,執行緒棧的大小等進行變化,而和Java堆的物件數量則並沒有任何關係;

所以,看到這裡,我們就應該可以理解了為什麼ZGC可以有膽量說,我們的GC回收過程中停頓時間絕對不會超過10ms的原因(注意:是停頓時間不會超過10ms,而不是整體回收時間不會超過10ms),因為從目前來看,真正會涉及到停頓時間的也只有,初始化標記過程,而初始化標記過程由於是和堆的大小以及數量是沒有關係的,所以這也才是ZGC之所以可以對外宣傳說,ZGC可以完全不受堆大大小限制,無論是執行在10G還是100G的堆上,ZGC都可以保證整個回收的停頓時間都不會超過10ms的原因;

當然,ZGC想要達到這樣的效果,實現起來則肯定沒有那麼簡單:
原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

ZGC的垃圾回收過程:

  1. 初始標記(stw)
  2. 併發標記
    2.1 併發標記後還會有一次短暫的stw暫停(Pause Mark End)詳情看上述的ZGC日誌分析;
  3. 併發預備重分配
    3.1 併發預備重分配後也還會有一次swt的暫停(Pause Relocate Start)詳情看上述的ZGC日誌分析
  4. 併發重分配
  5. 併發重對映

通過以上幾個階段可以得知,ZGC在實際的回收過程中,是有三次STW階段的,但是由於都與Java堆的物件大小沒有直接關係,所以stw的暫停時間也是極短的,整體下來仍然會滿足ZGC的宣傳口號,“不在意任何堆的大小都可以小於10ms”;

Q:ZGC是如何實現併發的清理物件的?
A:這裡涉及到一些很重的概念,也就是ZGC的一些核心概念,GC屏障 以及 染色指標技術;(這部分內容很原理化,所以此處則不再文章中重複贅述,關於染色指標和GC屏障的原理直接參考文章後續的參考連結即可)

Q:為什麼ZGC當前最大的管理堆記憶體不會超過4TB?
A:首先4TB的記憶體,其實對於目前的大多數場景都已經是足夠了,畢竟還有分散式節點呢。。。所以ZGC可以直接管理4TB的記憶體且還保持10ms的停頓特性,這已經很強了;但是深究為什麼ZGC只能管理不超過的4TB的記憶體?原因其實還是和ZGC內部所使用到的染色指標的技術相關(染色指標是一種直接將少量額外的資訊儲存在指標上的技術,目前在Linux下64位的作業系統中高18位是不能用來定址的,但是剩餘的46位卻可以支援64T的空間,到目前為止我們幾乎還用不到這麼多記憶體。於是ZGC將46位中的高4位取出,用來儲存4個標誌位,剩餘的42位可以支援4TB(2的42次冪)的記憶體,也就直接導致ZGC可以管理的記憶體不超過4TB)

ZGC的觸發階段

  1. rule_timer第一個策略,從行為表現上,我把它叫做是週期性GC,預設是不生效的,但是如果配置-XX:ZCollectionInterval=1(單位是秒),那麼每隔1s,就會執行一次ZGC;
  2. rule_warmupJVM啟動之後,如果一直沒有發生過GC,那麼會在堆記憶體使用超過10%、20%、30%時,分別觸發一次GC,這樣做是為了收集一些GC相關的資料,為後面的條件規則提供資料支撐。
  3. rule_allocation_rate根據物件分配速率決定是否GC。如果當前的可用堆記憶體,根據估計出來的物件最大分配速率,很快會被耗盡,則執行一次GC,這種策略一般在qps很高、物件分配很快時會被觸發。
  4. rule_proactive這個策略是積極主動型的。如果能夠接受因為GC引起的應用吞吐量下降,那麼就觸發GC,這個策略允許我們降低堆記憶體,並且在堆記憶體還有很多剩餘空間時,執行引用處理,具體的條件是(1、自從上次GC之後,堆的使用量至少漲了10%; 2、
    自從上次GC之後,已經過去5分鐘沒有發生GC)

ZGC 引數配置

ZGC現有的對外所提供的一些引數資訊:

General GC Options ZGC Options ZGC Dianostic Options (-XX:+UnlockDianosticVMOptions)
-XX:MinHeapSize, -Xms -XX:ZAllocationSpikeTolerance -XX:ZProactive
-XX:InitialHeapSize, -Xms -XX:ZCollectionInterval -XX:ZStatisticsForceTrace
-XX:MaxHeapSize, -Xmx -XX:ZFragmentationLimit -XX:ZStatisticsInterval
-XX:SoftMaxHeapSize -XX:ZMarkStackSpaceLimit -XX:ZVerifyForwarding
-XX:SoftRefLRUPolicyMSPerMB -XX:ZPath -XX:ZVerifyMarking
-XX:+UseNUMA -XX:ZUncommit -XX:ZVerifyObjects
-XX:ZUncommitDelay -XX:ZVerifyRoots
-XX:ZVerifyViews

引數解釋:

-XX:ConcGCThread:併發執行的GC執行緒數,預設JVM啟動的時候會設定為當前CPU核數的12.5%(注意,是併發執行緒數,也就是意味著配置的越大,那麼使用者執行緒的吞吐量就會變低,因為GC的執行緒會和應用執行緒存在搶佔CPU的情況)

-XX:ParallelGCThreads:GC ROOT的標記和移動時,也就是上述提到的3個STW的情況時,會使用該引數所配置的執行緒數來進行並行執行;(由於此引數所配置的執行緒數是STW期間並行執行的,所以相對來說多多益善嘍,不要超過CPU核數就行,預設情況下如果不設定預設為當前CPU核數的60%)

-XX:ZUncommit:這個引數很有意思,將會打破我們之前對Xmx和Xms最好配置為相等的這個觀念,以前在使用CMS,ParallelOld等GC時,預設情況下如果Xms和Xmx不一致,將會導致gc時的頻繁擴張,直到觸發到頂值也就是Xmx所配置的值;所以為了避免擴張,我們一般都會建議將Xmx和Xms配置為相同值;並且原有的JVM GC,即使GC後回收了很多的多餘空間,JVM也不會把這部分空間歸還給作業系統,也就是這部分記憶體儘管目前不會用到但也將一直被JVM所佔據;但是現在不同了!配置開啟當前Zuncommit引數後,預設情況下ZGC會把不再使用的記憶體歸還給作業系統,這樣對於特別在意記憶體佔用情況的伺服器或者說雲伺服器就特別有用了,記憶體資源的回收也就意味著雲伺服器的使用資源費率也將會降低;不過!!無論如何歸還,最終JVM也會保留Xms引數所指定的記憶體大小;也就是說如果Xms和Xmx配置一致,則該引數就基本沒用了;(合理的配置Xms的值將會特別有益於雲伺服器等)

-XX:ZUncommitDelay:預設300秒,這個參數列示不再使用的記憶體最多延遲多少時間後再歸還給作業系統,配合上述引數使用;

-Xlog:gc: 配置gc引數輸出位置,gc 表示輸出detail gc日誌詳情;

-XX:+UnlockExperimentalVMOptions: 表示開啟ZGC
-XX:+UseZGC:表示開啟ZGC

-XX:+UseNUMAZGC:預設是開啟支援NUMA的,不過,如果JVM探測到系統繫結的是CPU子集,就會自動禁用NUMA。我們可以通過引數-XX:+UseNUMA顯示啟動,或者通過引數-XX:-UseNUMA顯示禁用。如果執行在NUMA伺服器上,並且設定-XX:+UseNUMA,那對效能提升是顯而易見的。

配置一個自用的ZGC引數

簡單說明下,在ZGC的官網介紹上基準測試中的32和伺服器,128G堆的情況下,配置的ConcGCThread是4,配置的ParallelGCThreads是20;

而這兩個引數又恰恰是決定了ZGC回收併發&並行回收效率的很大一個變數,所以,我建議,在對自身應用服務的物件使用情況還不是很清晰的情況下,可以考慮使用預設的JVM值,也就是建議先不配置該值,避免弄巧成拙,如果後續gc的效率存在問題,則可以考慮觀察堆物件的生命週期以及gc的日誌來確定最優的配置方案;(注:官方對外給出的最大不會超過10ms的停頓時間,是在最優配置引數的情況下得到的結果,如果不合理的引數配置,導致gc回收停頓時間較長,那這個只能說是自身的問題的了、、、)

預設情況下直接配置如下的ZGC引數,其實就完事了:
-XX:+UnlockExperimentalVMOptions
-XX:+UseZGC
-XX:+UseNUMA
-Xms1g
-Xmx2g
-Xlog:gc*:C:\Arnold\workSpace\GC\jdk14gc.log

通過上述我們對ZGC的內部機制實現瞭解,就不會再出現直接環境上使用ZGC的回收器出現盲目慌亂的情況,畢竟瞭解他的機制和擴充套件過程,以及優化方式,就不會再存在任何好擔心的情況了。

部分名詞解釋

原創宣告:作者:Arnold.zhao 部落格園地址:https://www.cnblogs.com/zh94

STW

GC過程中常聽到的STW的含義實際上:Stop-The-World 也就是說在GC執行緒的回收過程中是要暫使用者的執行緒執行的,由於需要停止掉使用者的執行緒執行,所以在此期間整個應用程式是處於停頓狀態;

並行和併發的區別

上述在CMS以及G1和ZGC的部分內容說明中都提到了很多次的並行和併發的概念,此處做下相關解釋:並行和併發實際上都是多個執行緒同時執行,但是在此處的JVM GC場景中,並行一般是指的多個GC執行緒同時進行執行,這個叫做並行;而對於多個GC執行緒和使用者執行緒同時執行,則叫做併發;所以,G1的清理階段是並行的(stw),而cms和zgc則做到了併發的清理;

動態年齡計算機制

JVM中Eden區物件晉升至Old區時會涉及到一個物件動態年齡計算的問題,預設情況下當累積的某個年齡的物件大小超過了survivor區的一半時,則會取這個年齡和MaxTenuringThreshold中更小的一個值,作為新的晉升年齡閾值;(不過由於當前的ZGC是沒有分代的,所以也就沒有了晉升的概念了 ,但仍然適合於g1及cms處理器)

官方壓測對比回收效果

官方已經進行過具體的壓測對比了,此處跳過直接列舉部分網路資源。

停頓時間方面,ZGC是100%不超過10ms的

ZGC的垃圾回收方面,ZGC依然沒有做到整個GC過程完全併發執行,依然有3個STW階段,其他3個階段都是併發執行階段,對應的具體階段,可以直接通過上面的日誌分析就能得知

可參考外部連結

關於JDK14的新特性說明: https://zhuanlan.zhihu.com/p/98389056
關於ZGC的解讀: https://www.zhihu.com/topic/20208311/hot
關於G1收集器解讀:https://www.jianshu.com/p/548c67aa1bc0

g1日誌及zgc日誌註釋說明待補充完善;

Author:Arnold.zhao
Date:2020-06-16

相關文章