本文在第一段先簡單講解調優的緣由和過程,具體涉及到的知識點,在後面段中具體介紹。
1. 調優過程
1.1. 問題定位
有一天突然收到監控告警,大批量產線服務實例在自動重啓。於是趕緊上平台下載dump日誌,以及檢查其他監控事件,最終定位到問題:
那幾分鐘內,涌入幾十萬用户登錄平台操作,導致內存吃緊,幾乎每個實例都觸發了幾次 Full GC。而由於集中性的 Full GC,STW 時間過長,服務測活接口長期調不通,k8s判定服務故障,就重啓pod。
問題定位了,除了優化代碼,減少無效內存大量佔用以外,還可以調優一下Jvm參數了。
1.2. gc 問題定位
既然是 gc 出的問題,那就通過 jstat -gcutil pid 時長間隔 命令,實時看一下gc的過程狀態。
通過一段時間觀察,發現每次 young gc 後,survivor 區域中佔用比例很高(近百分之百),甚至某些次 old 區域中有略微增長。這説明一個問題:
young gc 後存活的對象太多,survivor區存放不下,溢出的對象就直接進入了老年代。這就加快了老年代內存的佔用速度,提前需要 full gc。
gc 的問題也定位到了,接下來分幾個步驟優化
1.3. gc 優化
分了幾個維度:
- 最直觀表現是 survivor 區不足,因此可以加大一下年輕代中 survivor 比例,即減少
-XX:SurvivorRatio(eden區和單個survivor區的比例,默認值:8)的值。 - 稍微加大一下年輕代的佔比,即減少
-XX:NewRatio(老年代和年輕代的比例,默認值:2)的值。 - 最根本的還是內存不足,所以如果可以,加大
xms/xmx。 - 調優垃圾收集器,減少 full gc 中 stw 的時間,避免測活接口長時間停機。因為之前是jdk 8默認的(Parallel Scavenge + Parallel Old),換成了更適合高併發的(ParNew + CMS)。
2. jvm命令及參數
2.1. jstat -gcutil
注意:出於保密考慮,實際的數據不能在文中展示,下列展示的是與上下文無關的服務數據,是比較正常的 gc 過程數據:
[root@xxxapi data]# jstat -gcutil 1 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
50.07 0.00 53.80 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 57.11 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 62.04 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 64.26 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 66.61 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 69.18 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 71.68 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 74.75 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 77.20 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 80.12 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 83.09 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 87.77 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 89.82 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 92.29 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 94.57 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
50.07 0.00 99.00 13.25 89.91 86.07 1082 21.299 5 2.055 23.354
0.00 74.61 3.38 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 7.88 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 11.46 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 14.93 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 18.23 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 21.29 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 25.22 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 28.74 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 31.64 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 36.85 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 39.30 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 44.76 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 48.55 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 51.25 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 54.17 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 58.48 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 61.99 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 64.52 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 67.25 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 70.92 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 74.60 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 78.43 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 82.41 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 86.26 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 90.79 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 93.74 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 95.90 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
0.00 74.61 99.28 13.25 89.91 86.07 1083 21.325 5 2.055 23.380
51.43 0.00 2.69 13.29 89.91 86.07 1084 21.345 5 2.055 23.400
51.43 0.00 5.53 13.29 89.91 86.07 1084 21.345 5 2.055 23.400
51.43 0.00 8.21 13.29 89.91 86.07 1084 21.345 5 2.055 23.400
- S0:倖存1區當前使用比例
- S1:倖存2區當前使用比例
- E:伊甸園區使用比例
- O:老年代使用比例
- M:元數據區使用比例
- CCS:壓縮使用比例
- YGC:年輕代垃圾回收次數
- YGCT:年輕代垃圾回收消耗時間
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
2.2. 對象進入老年代途徑
1. 對象年齡達到閾值後進入老年代
默認情況下,對象在新生代經歷了15次GC後,便會達到進入老年代的條件,將對象轉移進入老年代。當然,年齡的閾值可以通過JVM參數進行設置:
-XX:MaxTenuringThreshold=10
2. 大對象直接進入老年代
通過以下JVM參數進行設置:(注意此參數僅適用於Serial和ParNew兩款新生代收集器。)
-XX:PretenureSizeThreshold=5242880
原因:
- 大對象需要連續的內存空間,而新生代為了安放大對象可能需要多次進行GC,增加開銷;
- 新生代種伊甸園區和倖存者區常採用複製算法,需要經常複製對象到不同的區域,而大對象在複製時開銷較大。
3. 動態選擇進入老年代
HotSpot虛擬機並不一定會嚴格按照設置的年齡閾值,滿足以下條件也能直接進入老年代:Survivor 區中,年齡從 1 到 n 的對象大小之和超過 Survivor 區的 50% 時,新生代中年齡大於等於 n 的對象將進入老年代。
注意一個誤區:這個對象大小總和是按年齡從小到大累加的,並不是同齡對象。
4. young gc 後溢出的進入老年代
在 young gc後,正常存活對象放入 survivor區,但如果放不下,存活對象溢出的部分,就會被放入老年代。
3. 垃圾收集器
3.1. 年輕代
3.1.1. Serial
Serial是一類用於新生代的單線程收集器,採用複製算法進行垃圾收集。Serial進行垃圾收集時,不僅只用一條單線程執行垃圾收集工作,它還在收集的同時,所用的用户必須暫停。
- 優勢:簡單高效,由於採用的是單線程的方法,因此與其他類型的收集器相比,對單個cpu來説沒有了上下文之間的的切換,效率比較高。
- 缺點:會在用户不知道的情況下停止所有工作線程,用户體驗感極差,令人難以接受。
- 適用場景:Client 模式(桌面應用);單核服務器。
參數配置
-XX:+UserSerialGC:選擇Serial作為新生代垃圾收集器
3.1.2. ParNew
ParNew收集器其實就是Serial的一個多線程版本,其在單核cpu上的表現並不會比Serail收集器更好,在多核機器上,其默認開啓的收集線程數與cpu數量相等。
當用户線程都執行到安全點時,所有線程暫停執行,採用複製算法進行垃圾收集工作,完成之後,用户線程繼續開始執行。
- 優點:隨着cpu的有效利用,對於GC時系統資源的有效利用有好處。
- 缺點:和Serial是一樣的。
- 適用場景:ParNew是許多運行在Server模式下的虛擬機中首選的新生代收集器。因為CMS收集器只能與serial或者parNew聯合使用,在當下多核系統環境下,首選的是ParNew與CMS配合。ParNew收集器也是使用CMS收集器後默認的新生代收集器。
參數配置
-XX:UseParNewGC:新生代採用ParNew收集器-XX:ParallelGCThreads:設置JVM垃圾收集的線程數
3.1.3. Parallel Scavenge
Parallel Scavenge 也是一款用於新生代的多線程收集器,也是採用複製算法。與ParNew的不同之處在於:
Parallel Scavenge收集器的目的是達到一個可控制的吞吐量,而 ParNew 收集器關注點在於儘可能的縮短垃圾收集時用户線程的停頓時間。
所謂吞吐量就是CPU用於運行用户代碼的時間與CPU總消耗時間的比值, 即吞吐量=運行用户代碼時間/(運行用户代碼時間+垃圾收集時間)。
例如虛擬機一共運行了 100 分鐘,其中垃圾收集花費了 1 分鐘,那吞吐量就是 99% 。比如下面兩個場景:
- 垃圾收集器每 100 秒收集一次,每次停頓 10 秒;
- 垃圾收集器每 50 秒收集一次,每次停頓時間 7 秒。
雖然後者每次停頓時間變短了,但是總體吞吐量變低了,CPU 總體利用率變低了
- 優點:追求高吞吐量,高效利用CPU,是吞吐量優先,且能進行精確控制。
- 適用場景:注重吞吐量高效利用CPU,需要高效運算,且不需要太多交互。
參數配置
-XX:+UseParallelOldGC:默認使用 ParallelOldGC 時候默認新生代使用的是 ParallelScavenge 收集器-XX:MaxGCPauseMilis:控制最大垃圾收集停頓時間,參數值是一個大於0的毫秒數,收集器儘可能保證回收花費時間不超過設定值。但將這個值調小,並不一定會使系統垃圾回收速度更快,GC停頓時間是以犧牲吞吐量和新生代空間換來的。-XX:GCTimeRadio:設置吞吐量大小,參數值是一個(0,100)兩側均為開區間的整數。也是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。若把參數設置為19,則允許的最大GC時間就佔總時間的5%(1/(1+19))。默認值是99,即允許最大1%的垃圾收集時間。-XX:+UserAdaptiveSizePolicy:這是一個開關函數,當打開這個函數,就不需要手動指定新生代的大小,Eden與Survivor區的比例(-XX:SurvivorRatio,默認是8:1:1),晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等參數。JVM會動態調整這些參數,以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略.
3.2. 老年代
3.2.1. Serial Old
Serial Old是Serial收集器的老年代版本,同樣是一個單線程收集器,使用標記-整理算法。
- 適用場景:Client模式;單核服務器;與Parallel Scavenge收集器搭配;作為CMS收集器的後備方案,在併發收集發生Concurrent Mode Failure時使用
3.2.2. Parallel Old
Parallel Old是 Parallel Scavenge 收集器的老年代版本,使用 多線程和標記-整理算法,可以充分利用多核CPU的計算能力。
- 適用場景:注重吞吐量與CPU資源敏感的場合,與Parallel Scavenge 收集器搭配使用,jdk7 和 jdk8 默認使用該收集器作為老年代收集器。
參數配置
-XX:+UserParallelOldGC:開啓 ParallelScavenge + ParallelOld
3.2.3. CMS
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。採用的算法是“標記-清除”,運作過程分為四個步驟
- 初始標記,標記GC Roots 能夠直接關聯到達對象
- 併發標記,進行GC Roots Tracing 的過程
- 重新標記,修正併發標記期間因用户程序繼續運作而導致標記產生變動的那一部分標記記錄
- 併發清除,用標記清除算法清除對象。
其中“初始標記”和“重新標記”這兩個步驟仍然需要STW(stop the world)。耗時最長的“併發標記”與“併發清除”過程收集器線程都可以與用户線程一起工作。總體上來説CMS收集器的內存回收過程是與用户線程一起併發執行的。
- 優點:併發收集,低停頓
- 缺點:
- CMS收集器對CPU資源非常敏感,CMS默認啓動對回收線程數(CPU數量+3)/4,當CPU數量在4個以上時,併發回收時垃圾收集線程不少於25%,並隨着CPU數量的增加而下降,但當CPU數量不足4個時,對用户影響較大。
- CMS無法處理浮動垃圾,可能會出現“Concurrent Mode Failure”失敗而導致一次FullGC的產生。這時會地洞後備預案,臨時用SerialOld來重新進行老年代的垃圾收集。由於CMS併發清理階段用户線程還在運行,伴隨程序運行自然還會有新的垃圾產生,這部分垃圾出現在標記過程之後,CMS無法在當次處理掉,只能等到下一次GC,這部分垃圾就是浮動垃圾。同時也由於在垃圾收集階段用户線程還需要運行,那也就需要預留足夠的內存空間給用户線程使用,因此CMS收集器不能像其他老年代幾乎完全填滿再進行收集。可以通過參數
-XX:CMSInitiatingOccupancyFraction修改CMS觸發的百分比。 - 因為CMS採用的是標記清除算法,因此垃圾回收後會產生空間碎片。通過參數可以進行優化。
參數配置
-XX:+UseConcMarkSweepGC:啓用cms-XX:ConcGCThreads:併發的GC線程數-XX:+UseCMSCompactAtFullCollection:FullGC之後做壓縮整理(減少碎片)-XX:CMSFullGCsBeforeCompaction:多少次FullGC之後壓縮一次,默認是0,代表每次FullGC後都會壓縮一次-XX:CMSInitiatingOccupancyFraction:當老年代使用達到該比例時會觸發FullGC(默認是92,這是百分比)-XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設定的值),如果不指定,JVM僅在第一次使用設定值,後續則會自動調整-XX:+CMSScavengeBeforeRemark:在CMS GC前啓動一次minor gc,降低CMS GC標記階段(也會對年輕代一起做標記,如果在minor gc就幹掉了很多對垃圾對象,標記階段就會減少一些標記時間)時的開銷,一般CMS的GC耗時 80%都在標記階段-XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多線程執行,縮短STW-XX:+CMSParallelRemarkEnabled:在重新標記的時候多線程執行,縮短STW;
3.4. 總結
可以搭配使用的垃圾收集器包括:
- Serial + Serial old
- Serial + CMS
- ParNew + Serial old
- ParNew + CMS
- Parallel Scavenge + Serial 0ld
- Parallel Scavenge + Parallel 0ld
目前常用的搭配如下:
Parrallel Scavenge+Parrallel Old:JDK 8 默認收集器搭配。吞吐量優先,後台任務型服務適合;ParNew+CMS:經典的低停頓蒐集器,絕大多數商用、延時敏感的服務在使用;G1:JDK 9 默認收集器,堆內存比較大(6G-8G以上)的時候表現出比較高吞吐量和短暫的停頓時間;