目錄
1、暢想GC的目标
2、jvm調優的目标
3、GC調優時機
4、垃圾收集器的選擇
5、G1調優政策
6、G1垃圾收集實踐
6.1、JVM自動選擇垃圾收集器
6.2、G1垃圾收集
6.3、GC日志分析
7、小結
前言 c++和java之間有一堵由記憶體動态配置設定和垃圾收集技術所圍成的牆,牆外面的人想進去牆裡面的人想出來。
1、暢想GC的目标
詹姆斯· 高斯林 (James Gosling)是一名軟體專家,喊出了口号:“一次編寫,到處亂跑。” 他在1995年寫java這門程式設計語言的時候,可能并沒有想到java會如此廣泛的應用于web開發,沒有意識到要進行更多的web互動場景,應用對停頓時間要求是如此的嚴格,否則在剛開始設計垃圾回收的時候就不會粗暴的直接将應用線程停掉了。這個在現在來看是不太能接受的,随着jdk往上發展,web的高并發,互動場景的越來越頻繁, 是以要追求低停頓和高吞吐量成了程式員們的追求,是以垃圾收集器就需要與時俱進的進行不斷的優化,再優化,直到沒有停頓。 以至于出現了Z-GC,Z也不知道是不是zero的意思,代表着程式員們的極緻追求,沒有停頓時間。
The Z Garbage Collector (ZGC) is a scalable low latency garbage collector.
ZGC performs all expensive work concurrently, without stopping the execution of application threads for more than 10ms,
which makes is suitable for applications which require low latency and/or use a very large heap (multi-terabytes).
The Z Garbage Collector is available as an experimental feature,
and is enabled with the command-line options
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC.
ZGC的官網描述:Z-GC目标:能夠讓應用gc停頓的時間低于:10ms,适用于更大堆。
參考資料:Z-Gabage: https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0
2、jvm調優的目标
随着網際網路的web應用流量激增,堆記憶體空間的不斷增大,從官方垃圾收集器的一步步優化之路不難發現,程式員對JVM 的垃圾收集追求的目标在于以下三點:
- 吞吐量- Throughput;運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間)
- 停頓時間-P auseTime;垃圾收集器進行 垃圾回收中斷應用執行響應的時間
- GC的頻率-GCTimes;一般不做硬性要求,能接受一定程度的younggc,但一定要避免full-gc;
停頓時間越短就越适合需要和使用者互動的程式,良好的響應速度能提升使用者體驗; 高吞吐量則可以高效地利用CPU時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任務。 吞吐量和停頓時間也是評價一個垃圾收集器優劣的名額。 可能JVM對垃圾收集器追求的終極目标是:沒有停頓時間且擁有高吞吐量。 遺憾的是,目前還沒有這樣一款垃圾收集器問世,當然,也不是所有的應用都是追求停頓時間,可能有的不在乎時間而在乎吞吐量,是以我們目前所能做到的就是不斷調優,根據業務場景找到适應各自的項目需求的垃圾收集器,讓上面的三個主要的目标達到最優。
3、GC調優時機
什麼時候才需要調優?GC到底影響什麼?GC的常見症狀? 首先一定不是無聊,天馬行空的改參數,那樣反而适得其反。 例如 GC停頓導緻 常見的問題的症狀 :
- 系統CPU飙升很快;
- 系統運作的響應時間長,接口響應逾時;
- 網站經常不定期出現:長時間沒有響應的現象。
- gc次數太多,使用者線程代碼執行受影響,cpu使用會高?
- 記憶體的使用率逐漸增大,不夠用了;
4、垃圾收集器的選擇
jvm調優:如何調優才能實作我們的目标 呢,首先是垃圾收集器的選擇。 首先要明确,jvm的調優沒有萬能公式,每個項目背景和要求不同,調優的政策和參數都不一樣。 首先關于垃圾收集器的選擇:并不是并發度越高就越好的,停頓時間越短就越好。需要根據具體的情況來看。 官網垃圾收集器的選擇标準: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28
Selecting a Collector
首先讓jvm來自動選擇,不能滿足;
調整堆的大小,減少垃圾回收次數;
Unless your application has rather strict pause time requirements, first run your application and allow the VM to select a collector. If necessary, adjust the heap size to improve performance.
如果仍然不能滿足:
If the performance still does not meet your goals, then use the following guidelines as a starting point for selecting a collector.
記憶體小于100m,可以選擇serial收集器;
* If the application has a small data set (up to approximately 100 MB), then
select the serial collector with the option
-XX:+UseSerialGC.
單線程,使用serial;
* If the application will be run on a single processor and there are no pause time requirements, then let the VM select the collector, or select the serial collector with the option
-XX:+UseSerialGC.
沒有停頓時間的要求,關注吞吐量,選擇并行收集器;
* If (a) peak application performance is the first priority and (b) there are no pause time requirements or pauses of 1 second or longer are acceptable, then let the VM select the collector, or select the parallel collector with
-XX:+UseParallelGC.
關注停頓時間的要求,可以選擇G1;
* If response time is more important than overall throughput and garbage collection pauses must be kept shorter than approximately 1 second, then select the concurrent collector with -XX:+UseConcMarkSweepGC or
-XX:+UseG1GC.
根據官網推薦,垃圾收集器的選擇标準總結如下:
- 優先調整堆的大小讓伺服器jvm自己來選擇一個合适的垃圾收集器;
- 如果記憶體小于100M,使用串行收集器;
- 如果是單核,并且沒有停頓時間要求,使用串行或JVM自己選
- 如果允許停頓時間超過1秒,選擇并行CMS或JVM自己選;
- 如果響應時間最重要,并且不能超過1秒,使用并發收集器G1;
從結論可以看出:選擇一個合适的垃圾收集需要根據系統的要求:
- 比如cpu的核心數:如果是單核cpu,選擇并發垃圾收集器也沒有用,因為單核還是串行的,線程的切換反而降低了垃圾收集的效率;
- 堆的大小:類似G1垃圾收集,它的記憶體布局讓它更适合大堆記憶體的收集,而小堆記憶體串行和CMS就能有比較高的性能;
- 是否關注停頓時間:如果不關注停頓時間關注吞吐量,串行和CMS就能提供很好的性能,也并不是G1就是最好的;
5、G1調優政策
官方也給出了G1調優的一些建議指南: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
- (1)不要手動設定新生代和老年代的大小,隻要設定整個堆的大小
- G1收集器在運作過程中,會自己調整新生代和老年代的大小 其實是通過自動調整young代的大小 來調整對象晉升的速度,進而達到為收集器設定的暫停時間目标,如果手動設定了大小就意味着放棄了G1的自動調優, 破壞了停頓時間政策;
- (2) 不斷調優暫停時間;不要太嚴格
- 一般情況下這個值設定到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設定成50ms就不太合理。
- 暫停時間設定的太短,就會導緻出現G1跟不上垃圾産生的速度。最終退化成Full GC。
- 是以對這個參數的調優是一個持續 的過程,逐漸調整到最佳狀态。暫停時間隻是一個目标,并不能總是得到滿足。
- (3)使用-XX:ConcGCThreads=n來增加标記線程的數量
- IHOP如果閥值設定過高,可能會遇到轉移失敗的風險,比如對象進行轉移時空間不足。如果閥值設定過低,就會使标 記周期運作過于頻繁,并且有可能混合收集期回收不到空間。 IHOP值如果設定合理,但是在并發周期時間過長時,可以嘗試增加并發線程數,調高ConcGCThreads。
- (4)MixedGC調優
- -XX: InitiatingHeapOccupancyPercent=45 :觸發并發标記的堆記憶體使用占比;
- -XX:G1MixedGCLiveThresholdPercent
- -XX:G1MixedGCCountTarger
- -XX:G1OldCSetRegionThresholdPercent
- (5)條件允許的情況下,适當增加堆記憶體大小
6、G1垃圾收集實踐
一般如果發現gc頻繁,或者gc停頓時間長不可接受,我們就需要對gc的參數進行調整,然後通過日志,調整參數,達到一個GC停頓時間和吞吐量的最佳的狀态,我們将用以下代碼來模拟檢視堆區的gc日志來進一步了解jvm各垃圾收集的工作過程,由于CMS之前分析過,這裡不在贅述,主要分析下G1收集器。
6.1、JVM自動選擇垃圾收集器
首先我們自己不設定垃圾收集器,讓JVM自己來為我們選擇,因為官方推薦這麼做,當不知道如何選擇的時候可以把這個權限交給JVM,JVM會預設的選擇一個垃圾回收器,用下面一小段代碼來不斷産生垃圾,看垃圾收集器的作用及日志。
public class HeapOomGCTest {
public static String heapOOMtest() throws InterruptedException {
List<Person> list = new ArrayList<Person>();
while (true) {
list.add(new Person());
System.out.println("add Person success~");
Thread.sleep(10);
}
}
public static void main(String[] args) throws InterruptedException {
heapOOMtest();
}
}
設定參數如下:
-Xms30m -Xmx30m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:JvmAutoSelectGC.log

JvmAutoSelectGC.log日志解讀:
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for bsd-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:35:23 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 16777216k(651656k free)
/proc/meminfo:
CommandLine flags: -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=31457280 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails
-XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
2020-11-22T09:05:50.054-0800: 10.188: [GC (Allocation Failure) [PSYoungGen: 8192K->1008K(9216K)] 8192K->1276K(29696K),
0.0019565 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
日志具體内容分析:
-XX:+UseParallelGC :通過日志發現在沒有設定GC垃圾收集器的情況下, JDK1.8預設使用的垃圾收集器:ParallerGC = ParallerScavge + ParallerOld GC:表明進行了一次垃圾回收,前面沒有Full修飾,PSYoungGen表明這是一次新生代的Minor GC,這裡不管是新生代還是老年代都會STW。 Allocation Failure:表明本次引起GC的原因是因為在年輕代中沒有足夠的空間能夠存儲新的資料了。 8192K->1008K(9216K):(機關是KB)三個參數分别為:GC前該記憶體區域(這裡是年輕代)使用容量,GC後該記憶體區域使用容量,該記憶體區域總容量。 因為我設定的總堆大小為30M=30720kb,-XX:NewRatio=2,出去其他的記憶體占用,是以新生代Eden區的總容量為:9216kb 0.0019565 secs:該記憶體區域GC耗時,機關是秒 8192K->1276K(29696K):三個參數分别為:堆區垃圾回收前的大小,堆區垃圾回收後的大小,堆區總大小。 [Times: user=0.00 sys=0.01, real=0.00 secs]:分别表示使用者态耗時,核心态耗時和總耗時
6.2、G1垃圾收集
參數設定如下:
-Xms500m -Xmx500m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseG1GC -XX:MaxGCPauseMillis=15 -Xloggc:G1-gc.log
CommandLine flags: -XX:CMSInitiatingOccupancyFraction=30 -XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintGC
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC #使用 G1垃圾收集;
# 什麼時候發生的GC,相對的時間刻,GC發生的區域young,總共花費的時間,0.00478s,
2020-11-22T13:45:42.218-0800: 0.196: [GC pause (G1 Evacuation Pause) (young), 0.0018535 secs]
# 表示8個垃圾回收線程,并行的時間
[Parallel Time: 1.2 ms, GC Workers: 8]
# GC線程開始相對于上面的0.196的時間刻
[GC Worker Start (ms): Min: 122.3, Avg: 122.5, Max: 123.0, Diff: 0.7]
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.9, Diff: 0.9, Sum: 1.9]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 1.8]
[Termination (ms): Min: 0.0, Avg: 0.4, Max: 0.5, Diff: 0.5, Sum: 3.0]
[Termination Attempts: Min: 1, Avg: 1.8, Max: 4, Diff: 3, Sum: 14]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.3, Avg: 0.8, Max: 1.1, Diff: 0.8, Sum: 6.8]
[GC Worker End (ms): Min: 123.3, Avg: 123.4, Max: 123.5, Diff: 0.1]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.5 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.3 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.2 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->1024.0K Heap: 1024.0K(6144.0K)->536.1K(6144.0K)]
[Times: user=0.01 sys=0.00, real=0.00 secs]
G1-GC.log部分日志截取如下:
從上面的日志來看,其日志格式複雜了很多,可以參考G1日志詳細解讀:https://blogs.oracle.com/poonam/understanding-g1-gc-logs
6.3、GC日志分析
6.3.1、本地工具檢視:gc-viewer
使用指令運作:
java -jar gcviewer-1.36-SNAPSHOT.jar
運作這個工具jar包,打開我們剛剛生成的G1-GC.log檔案,我們就可以看見jvm調優的幾個核心名額,其實從工具我們也可以清楚的看到jvm的gc調優關注的是什麼,調的是什麼,停頓時間-吞吐量-GC頻率。
可以發現,通過此工具清楚的看到 g1發生gc的詳情, 停頓耗時,gc次數,吞吐量都一目了然,之前我們的設定參數如下,設定的比較小,這樣效果比較明顯。
-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:CMSInitiatingOccupancyFraction=30 -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:MaxGCPauseMillis=5 -XX:+UseG1GC -Xloggc:G1-GC.log
我們發現其核心名額如下:
吞吐量 | 最小停頓時間 | 平均停頓時間 | 最大停頓時間 | gc次數 |
97.25 | 0.00048 | 0.00254 | 0.00671 | 13 |
從這幾個參數我們就可以看出停頓時間,吞吐量和gc的次數是否符合我們的要求,如果不符合我們可以繼續調整參數。 調整方案:
- 如果gc頻次高,我們可以适當的增加堆記憶體,這樣可能會增加gc的停頓時間;
- 然後我們可以适當的控制減小gc的停頓時間,但不要太嚴格;如果頻次還是高;
- 可以修改觸發G1垃圾回收的門檻值 -XX:InitialHeapOccupacyPersent,預設是45%,可以适當的提高;
最終達到頻次,停頓時間和吞吐量的一個最優值; 6.3.2、線上工具:GCeasy工具 這個工具也可以看到gc的詳細過程及日志情況,還可以比較不同的垃圾收集器的吞吐量和停頓時間,非常友善,但是線上的工具需要注意安全性。 官網位址 : https://gceasy.io 打開我們剛剛生成的G1-gc.log日志,從官網的顯示也可以看出gc調優的三個重要名額: 吞吐量 + 停頓時間 + gc頻次
從結果可以清晰的看到jvm調優的三個核心名額的資料,不同工具好像統計出的有一定的差别,是以我們可以通過這些工具來幫助我們更好的分析gc日志。
7、小結
GC的垃圾收集器的參數和選擇都不唯一,需要根據項目的場景及硬體條件作出選擇,适合的就是最好的,沒有銀彈。
- gc調優核心名額
- 吞吐量 — throughtput
- 停頓時間 —pause time
- gc頻率 — gc times
- GC日志檢視工具:
- GCeasy:線上工具;
- GCviewer:本地工具;java -jar gc-viewer.jar
OK---望着大河彎彎,終于敢放膽,嬉皮笑臉面對,人生的難。 水滴石穿,積少成多。學習筆記,内容簡單,用于複習,梳理鞏固。 參考資料: 《深入了解jvm虛拟機》 Z-Gabage: https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0 G1日志詳細解讀: https://blogs.oracle.com/poonam/understanding-g1-gc-logs G1調優的一些建議指南: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations