場景描述
相信大家都了解 jps、jmap、jstack 等常用 java 堆棧輸出命令,有過 dump、gc 分析的經驗,面試中會經常被問到有關 JVM 問題,比如你是否了解你的程序在生產環境的基礎配置,堆內存、棧內存怎么設置的,又是怎么估算的大小,或是垃圾回收器及回收垃圾算法的最佳使用策略。作為項目的核心開發人員,別把這些事當成是架構師要干的活,因為代碼可是你一行一行碼出來的,沒人比你更清楚,你得負責從程序開發、黑白盒測試、項目驗收、部署上線、集成交付、運維監控、用戶體驗等環節。越大的企業,項目模塊分配的越細,這也並不代表你不需要了解整體系統的性能,其中任何一個環節出問題,都可能導致系統無法正常運行。
借由這次生產系統頻繁宕機,我們總結一下 JVM 內存模型划分、JVM 啟動堆內存相關參數配置及說明、各年齡代的垃圾回收器及回收過程、生產 GC 日志解讀與分析、系統運行內存預估方法、啟動參數如何優化等。希望通過這篇小記來和大家一起交流、一起學習。
正文
2.1 生產 GC日志文件
部分截圖如下:

2.2 先看一下 jdk 1.8 的內存划分情況
按年齡划分為年輕代、老年代、元空間、本地方法區、虛擬機棧和程序計數器。下圖詳細說明了這幾個內存分區的關系、JVM 參數說明、存儲的相關內容及各內存分區的垃圾回收器及垃圾回收算法。

2.3 生產基礎環境
說明如下:
JDK版本:jdk_1.8
Web容器:Tomcat
題外話:估計市面上都是玩微服務了吧,jdk 版本至少也得 1.8 以上,jdk 1.6 不支持 G1 這么好用的垃圾收集器,也不支持 lambda 表達式,以及其他好用的特性
2.4 生產 JVM 堆內存相關參數
設置如下:
// 初始堆大小-Xms4096M// 最大堆大小-Xmx4096M// 持久代最大值-XX:MaxPermSize=1024M//......
題外話:這份配置一看就有點問題,為什么到現在才發現,因為系統之前很少出現問題,之前也未設置GC日志記錄參數,也未曾關心 JVM 參數設置,大家只是在原有的工程進行開發和維護。其中 -Xmn 年輕代未配置(-XX:NewRatio 年輕代與年老代所占比值也未配置),-XX:PermSize 持久代初始值未配置(存在動態擴容帶來的性能消耗)等
2.5 截取生產一條 GC 日志
圖解分析如下:
2019-11-20T17:15:38.906+0800: 672725.775: [GC 2019-11-20T17:15:38.907+0800: 672725.776: [ParNew: 143735K->15199K(153344K), 0.0485240 secs] 2568043K->2439507K(4177280K), 0.0497750 secs] [Times: user=0.20 sys=0.00, real=0.05 secs]

從以上 GC 日志文件結構圖解可以清晰看出,線上生產環境的年輕代總內存大小分配約 150M,堆總內存大小約 4G,明顯年輕代內存分配過小。每次 ParNew GC 老年代變化可以由堆內存大小變化和年輕代內存大小變化推算。
從下圖 GC 日志可以看出,線上系統出現頻繁 ParNew GC(即年輕代的 Minor GC),平均大約每 5 分鍾進行一次 Minor GC,即一天平均執行 288 次之多,太可怕了吧!!!唉

題外話:為什么這么頻繁,系統都線上運行3年了,當初系統上線JVM啟動參數應該是隨便設置的,呵呵一是系統並發量不高,二是用戶量不大,三是開發人員不注重JVM優化,四是到前不久才加上GC日志輸出參數,五是 pinpoint 運維監控系統居然不支持 Minor GC的監控,只支持 Full GC 監控,呵呵
2.6 CMS (Concurrent Mark Sweep)
CMS 垃圾回收器進行一次 Full GC,GC日志部分截圖如下所示:

從上圖可以看出,CMS 垃圾回收器正常運行(CMS 垃圾回收觸發的條件:當老年代內存達到92%(3719000K / 4023936K * 100% = 92%),詳情見下圖)。對上圖 CMS GC 進行剖析如下:

從圖中可以清晰看到,CMS 對於老年代的垃圾回收分成 7 個階段,每個階段到底做了什么,(這個圖很重要,是CMS垃圾回收器工作過程詳解,其中主要是分了四個過程,初始化標記(會發生STW)、並發標記、重新標記(會發生STW)、並發清除,詳見垃圾回收器)詳情見以下流程圖所示:

2.7 隨着用戶量增加、系統並發增加
系統出現了頻繁 Full GC,pinpoint(是一個JVM內存監控軟件)監控內存使用情況如下(只能監控老年代的 Full GC,而無法監控年輕代的 Minor GC,其實 Full GC 之前 Minor GC 執行次數頻率更可怕):

2.8 ParNew + CMS 組合
ParNew(年輕代垃圾回收器) + CMS(老年代垃圾回收器) 回收器組合是在 JDK 1.8 之前大多數 JAVA 企業級服務應用的最佳選擇,從以下生產 GC 日志截圖中可以看到,在 CMS 回收器觸發時,出現了 promotion failed 和 concurrent mode failure 現象:

針對這兩個現象產生的原因進行解讀如下:
- promotion failed該現象是在進行觸發年輕代 ParNew GC 時,存活的對象在 Survivor 區放不下,對象只能進入老年代,而此時老年代也放不下導致的。
- concurrent mode failure該現象是在執行 CMS 回收器回收垃圾的過程中同時有存活的對象放入老年代,而此時老年代空間不足,或者在做 ParNew GC 的時候,年輕代 Survivor 區放不下,需要放入老年代,而老年代也放不下而導致的。
2.9 解決方案
針對以上2種現象產生的原因進行 JVM 相關參數優化:可增大年輕代或者 Survivor 區的存儲空間
-Xmn1500M
-XX:SurvivorRatio=8
或者提前觸發 CMS 垃圾回收和進行 5 次 CMS 垃圾回收后整理清除碎片
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=80
2.10 最后對生產環境的 JVM 內存參數設置進行優化
建議虛擬機參數設置如下:
-Xms4096M
-Xmx4096M
-Xmn1500M
-XX:PermSize=1024M
-XX:MaxPermSize=1024M
-Xss512K
-XX:SurvivorRatio=8
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=80
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:log/gc.log
線上系統內存估算方法
3.1 Java對象屬性類型所占字節大小
列表清單如下:

3.2 Java對象所占JVM內存結構
如下圖展示:


可以看到數組類型對象和普通對象的區別僅在於 4 字節數組長度的存儲區間。而對象指針究竟是 4 字節還是 8 字節要看是否開啟指針壓縮。Oracle JDK 從 6_update_23 開始在 64 位系統上會默認開啟壓縮指針。如果要強行關閉指針壓縮使用 -XX:-UseCompressedOops,強行啟用指針壓縮使用:-XX:+UseCompressedOops。
假如生產訂單某一對象大約30字段,如訂單對象 JavaBeanA ,所占內存大小計算的方法如下所示:
public class ObjectA { int a; // 4 Byte
byte b; // 1 Byte
String c; // 4 Byte
double d; // 8 Byte
String e; // 4 Byte // 此處省略25個String對象 25*4 Byte ObjectB objB; // 8 Byte }
public class ObjectB { // ... }
Size(ObjectA) = Size(對象頭(_mark)) + size(oop指針) + size(數據區)Size(ObjectA) = 8 + 4 + 4(int) + 1(byte) + 4(String) * 26 + 8(double) + 7(padding) + 8(ObjectB指針)Size(ObjectA) = 136 字節 = 136 / 1024 kb = 0.133 kb
由此,可以大約估算出你的線上系統每秒產生多少 M 的對象。如果每秒產生 500 個 ObjectA,即大約 0.5 M,那么對於年輕代 1500M 的內存,大約需要 3000s 充滿,即 50 min才觸發一次 Minor GC,也就是說一天大約觸發24次 Minor GC
總結
- 對於生產系統,合理增大年輕代內存大小,本着盡量減少系統 Minor GC,一日最多一次 Full GC 的原則;
- 優化編碼,減少不必要的對象創建,合理定義對象,合理使用和優化數據結構;
- 優化 JVM 內存參數以減少 GC 次數,生產選擇換最優垃圾收集器配置策略。
