JDK14-ZGC調研初探


原創聲明:作者: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM