JVM 調校是將理論知識應用於實際系統的關鍵環節。本章將介紹常用的監控工具、診斷方法和調校策略,幫助你解決生產環境中的效能問題。
調校方法論#
調校原則#
不要過早最佳化,不要盲目最佳化。 調校的前提是有明確的效能問題和最佳化目標。先監控、分析、定位問題,再針對性最佳化。
┌─────────────────────────────────────────────────────────────────────┐
│ JVM 調校流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 監控 (Monitor) │
│ │ 收集 GC 日誌、記憶體使用、CPU 使用等資料 │
│ ▼ │
│ 2. 分析 (Analyze) │
│ │ 分析 GC 頻率、暫停時間、記憶體洩漏等 │
│ ▼ │
│ 3. 定位 (Locate) │
│ │ 找出瓶頸:是 GC 問題?記憶體問題?還是程式碼問題? │
│ ▼ │
│ 4. 調校 (Tune) │
│ │ 調整 JVM 參數或修改程式碼 │
│ ▼ │
│ 5. 驗證 (Verify) │
│ │ 驗證最佳化效果,確保沒有引入新問題 │
│ ▼ │
│ 6. 重複 │
│ 持續監控,發現新問題再次調校 │
│ │
└─────────────────────────────────────────────────────────────────────┘調校目標#
| 目標 | 關鍵指標 | 適用場景 |
|---|---|---|
| 低延遲 | GC 暫停時間 < 目標值 | 即時系統、交易系統 |
| 高吞吐量 | GC 時間佔比 < 5% | 批次處理、資料分析 |
| 低記憶體 | 堆積使用率在合理範圍 | 資源受限環境 |
監控工具#
jps - 查看 Java 程序#
# 列出所有 Java 程序
jps
# 顯示完整主類別名
jps -l
# 顯示 JVM 參數
jps -v
# 顯示傳遞給 main 方法的參數
jps -mjstat - JVM 統計資訊#
# GC 統計(每 1000ms 輸出一次,共 10 次)
jstat -gc <pid> 1000 10
# GC 原因
jstat -gccause <pid> 1000
# 堆積記憶體使用率
jstat -gcutil <pid> 1000
# 類別載入統計
jstat -class <pid>
# JIT 編譯統計
jstat -compiler <pid>jstat -gcutil 輸出解讀:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 50.00 45.23 68.15 95.12 92.34 125 2.345 3 0.567 2.912| 欄位 | 說明 |
|---|---|
| S0, S1 | Survivor 0/1 使用率 |
| E | Eden 使用率 |
| O | Old 老年代使用率 |
| M | Metaspace 使用率 |
| CCS | 壓縮類別空間使用率 |
| YGC | Young GC 次數 |
| YGCT | Young GC 總耗時(秒) |
| FGC | Full GC 次數 |
| FGCT | Full GC 總耗時(秒) |
| GCT | GC 總耗時 |
jmap - 記憶體映射#
# 堆積記憶體摘要
jmap -heap <pid>
# 物件直方圖(按佔用記憶體排序)
jmap -histo <pid>
# 只顯示存活物件(會觸發 Full GC)
jmap -histo:live <pid>
# 產生堆積傾印
jmap -dump:format=b,file=heap.hprof <pid>
# 產生堆積傾印(只包含存活物件)
jmap -dump:live,format=b,file=heap.hprof <pid>在生產環境執行
jmap -dump會導致 STW,可能影響服務。建議使用-XX:+HeapDumpOnOutOfMemoryError自動產生傾印。
jstack - 執行緒堆疊#
# 輸出執行緒堆疊
jstack <pid>
# 輸出到檔案
jstack <pid> > thread_dump.txt
# 強制輸出(當程序無回應時)
jstack -F <pid>
# 輸出鎖資訊
jstack -l <pid>執行緒狀態說明:
執行緒狀態:
├── NEW - 新建立,尚未啟動
├── RUNNABLE - 可執行(可能在執行或等待 CPU)
├── BLOCKED - 阻塞(等待監視器鎖)
├── WAITING - 無限期等待
├── TIMED_WAITING- 限時等待
└── TERMINATED - 終止死鎖偵測範例
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f1234560000 (object 0x000000076ab12345, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f1234561000 (object 0x000000076ab12346, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadlockDemo.method2(DeadlockDemo.java:25)
- waiting to lock <0x000000076ab12345> (a java.lang.Object)
- locked <0x000000076ab12346> (a java.lang.Object)
"Thread-0":
at DeadlockDemo.method1(DeadlockDemo.java:15)
- waiting to lock <0x000000076ab12346> (a java.lang.Object)
- locked <0x000000076ab12345> (a java.lang.Object)
Found 1 deadlock.jcmd - 多功能診斷工具#
# 列出可用命令
jcmd <pid> help
# VM 資訊
jcmd <pid> VM.info
# 系統屬性
jcmd <pid> VM.system_properties
# VM 旗標
jcmd <pid> VM.flags
# 執行緒堆疊
jcmd <pid> Thread.print
# 堆積傾印
jcmd <pid> GC.heap_dump /path/to/dump.hprof
# 觸發 GC
jcmd <pid> GC.run
# 類別直方圖
jcmd <pid> GC.class_histogram圖形化工具#
JConsole#
# 啟動 JConsole
jconsole
# 連線遠端 JVM
jconsole <hostname>:<port>VisualVM#
# 啟動 VisualVM
visualvm
# 安裝外掛擴展功能
# Tools -> PluginsVisualVM 功能:
- 即時監控 CPU、記憶體、執行緒
- 堆積傾印分析
- 執行緒分析與死鎖偵測
- CPU 和記憶體 Profiling
- GC 日誌視覺化
Eclipse Memory Analyzer (MAT)#
# 開啟堆積傾印檔案
# File -> Open Heap Dump
# 常用報告
# - Leak Suspects Report(洩漏嫌疑報告)
# - Top Consumers(記憶體消耗大戶)
# - Dominator Tree(支配樹)MAT 是分析記憶體洩漏的利器。Leak Suspects Report 會自動分析可能的洩漏點,Dominator Tree 可以找出哪些物件「支配」了大量記憶體。
GC 日誌分析#
啟用 GC 日誌#
# Java 8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:/path/to/gc.log
# Java 9+(統一日誌框架)
-Xlog:gc*:file=/path/to/gc.log:time,uptime,level,tags
# 更詳細的日誌
-Xlog:gc*,gc+heap=debug,gc+phases=debug:file=/path/to/gc.log:time,uptime,level,tagsGC 日誌分析工具#
GCViewer#
# 下載並執行
java -jar gcviewer-1.36.jar gc.logGCEasy(線上工具)#
上傳 GC 日誌到 https://gceasy.io
自動產生分析報告,包括:
- GC 暫停時間分佈
- 記憶體使用趨勢
- 效能建議G1 GC 日誌範例分析#
[2024-01-15T10:30:45.123+0800][gc,start ] GC(12) Pause Young (Normal) (G1 Evacuation Pause)
[2024-01-15T10:30:45.125+0800][gc,task ] GC(12) Using 4 workers of 8 for evacuation
[2024-01-15T10:30:45.130+0800][gc,phases ] GC(12) Pre Evacuate Collection Set: 0.2ms
[2024-01-15T10:30:45.130+0800][gc,phases ] GC(12) Merge Heap Roots: 0.1ms
[2024-01-15T10:30:45.130+0800][gc,phases ] GC(12) Evacuate Collection Set: 4.5ms
[2024-01-15T10:30:45.130+0800][gc,phases ] GC(12) Post Evacuate Collection Set: 0.3ms
[2024-01-15T10:30:45.130+0800][gc,phases ] GC(12) Other: 0.1ms
[2024-01-15T10:30:45.130+0800][gc,heap ] GC(12) Eden regions: 51->0(48)
[2024-01-15T10:30:45.130+0800][gc,heap ] GC(12) Survivor regions: 7->10(10)
[2024-01-15T10:30:45.130+0800][gc,heap ] GC(12) Old regions: 35->35
[2024-01-15T10:30:45.130+0800][gc,heap ] GC(12) Humongous regions: 2->2
[2024-01-15T10:30:45.130+0800][gc ] GC(12) Pause Young (Normal) (G1 Evacuation Pause) 388M->190M(512M) 5.2ms解讀要點:
Pause Young (Normal):正常的 Young GCUsing 4 workers:使用 4 個 GC 執行緒388M->190M(512M):GC 前 388MB → GC 後 190MB,堆積總大小 512MB5.2ms:GC 暫停時間
常用 JVM 參數#
記憶體設定#
# 堆積大小
-Xms4g # 初始堆積
-Xmx4g # 最大堆積(建議與 Xms 相同)
# 新生代
-Xmn2g # 新生代大小
-XX:NewRatio=2 # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1
# Metaspace
-XX:MetaspaceSize=256m # 初始 Metaspace
-XX:MaxMetaspaceSize=512m # 最大 Metaspace
# 堆疊
-Xss1m # 執行緒堆疊大小
# 直接記憶體
-XX:MaxDirectMemorySize=256mGC 設定#
# 選擇 GC
-XX:+UseG1GC # G1(Java 9+ 預設)
-XX:+UseZGC # ZGC(低延遲)
-XX:+UseParallelGC # Parallel(高吞吐量)
# G1 調校
-XX:MaxGCPauseMillis=200 # 目標暫停時間
-XX:G1HeapRegionSize=4m # Region 大小
-XX:InitiatingHeapOccupancyPercent=45 # 觸發併發標記
# 物件晉升
-XX:MaxTenuringThreshold=15 # 晉升閾值
-XX:PretenureSizeThreshold=1m # 大物件直接進入老年代診斷與監控#
# GC 日誌
-Xlog:gc*:file=gc.log:time,uptime,level,tags
# 堆積傾印
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump
# 錯誤檔案
-XX:ErrorFile=/path/to/error.log
# JMX 遠端監控
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false效能最佳化#
# 編譯最佳化
-XX:+TieredCompilation # 分層編譯(預設開啟)
-XX:CompileThreshold=10000 # 編譯閾值
# 逃逸分析
-XX:+DoEscapeAnalysis # 啟用逃逸分析
-XX:+EliminateAllocations # 標量替換
-XX:+EliminateLocks # 鎖消除
# 大頁(需要 OS 支援)
-XX:+UseLargePages
# 字串去重(G1 專用)
-XX:+UseStringDeduplication常見問題與解決方案#
1. 頻繁 Full GC#
症狀:
Full GC 頻繁發生,每次暫停時間長,應用程式回應變慢排查步驟:
# 1. 查看 GC 日誌
grep "Full GC" gc.log
# 2. 分析 GC 原因
jstat -gccause <pid> 1000
# 3. 堆積使用情況
jstat -gcutil <pid> 1000常見原因與解決:
| 原因 | 解決方案 |
|---|---|
| 老年代空間不足 | 增加堆積大小或調整新生代比例 |
| Metaspace 不足 | 增加 MaxMetaspaceSize |
| 顯式呼叫 System.gc() | 加上 -XX:+DisableExplicitGC |
| 記憶體洩漏 | 使用 MAT 分析堆積傾印 |
2. 記憶體洩漏#
症狀:
堆積使用率持續上升,Full GC 後記憶體釋放很少排查步驟:
# 1. 產生堆積傾印
jmap -dump:format=b,file=heap.hprof <pid>
# 2. 使用 MAT 分析
# - 開啟 Leak Suspects Report
# - 查看 Dominator Tree
# - 分析 GC Roots 路徑
# 3. 對比多個時間點的物件數量
jmap -histo <pid> > histo1.txt
# 等待一段時間
jmap -histo <pid> > histo2.txt
diff histo1.txt histo2.txt常見洩漏模式:
// 1. 靜態集合持有物件
static List<Object> cache = new ArrayList<>();
public void add(Object obj) {
cache.add(obj); // 只加不移除
}
// 2. 內部類別持有外部類別參照
class Outer {
byte[] data = new byte[1024 * 1024];
class Inner {
// Inner 持有 Outer 的參照
// 如果 Inner 存活,Outer 就無法回收
}
}
// 3. 資源未關閉
Connection conn = dataSource.getConnection();
// 忘記關閉 conn3. CPU 使用率過高#
排查步驟:
# 1. 找出 CPU 高的執行緒
top -Hp <pid>
# 2. 將執行緒 ID 轉為 16 進位
printf '%x\n' <tid>
# 3. 在執行緒傾印中搜尋
jstack <pid> | grep -A 20 <hex_tid>
# 4. 分析熱點程式碼
# 或使用 async-profiler 進行 CPU Profiling
./profiler.sh -d 30 -f profile.html <pid>4. 執行緒阻塞#
症狀:
應用程式回應慢,執行緒堆積中大量 BLOCKED 或 WAITING 狀態排查步驟:
# 1. 產生執行緒傾印
jstack <pid> > thread.txt
# 2. 統計執行緒狀態
grep "java.lang.Thread.State" thread.txt | sort | uniq -c
# 3. 找出等待的鎖
grep -A 1 "waiting to lock" thread.txt
# 4. 偵測死鎖
jstack -l <pid> | grep -A 50 "deadlock"調校案例#
案例 1:電商系統 GC 調校#
問題描述:
- 電商促銷期間,系統回應變慢
- 監控顯示 Young GC 頻繁,偶爾有長時間 Full GC
分析:
# GC 統計
S0 S1 E O M YGC YGCT FGC FGCT
0.00 80.00 95.12 75.23 95.00 1523 45.67 12 8.234
# 問題:
# - YGC 頻繁(1523 次),平均每次 30ms
# - Eden 區經常接近滿
# - Old 區使用率高,Full GC 時間長調校方案:
# 原始組態
-Xms4g -Xmx4g -XX:NewRatio=2
# 最佳化組態
-Xms8g -Xmx8g \
-Xmn3g \ # 增大新生代
-XX:SurvivorRatio=8 \
-XX:MaxTenuringThreshold=10 \ # 降低晉升閾值
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \ # 目標暫停時間
-XX:G1HeapRegionSize=8m最佳化結果:
- Young GC 頻率降低 50%
- Full GC 次數大幅減少
- 平均回應時間改善 30%
案例 2:批次處理系統調校#
問題描述:
- 夜間批次處理任務執行時間過長
- GC 日誌顯示吞吐量只有 90%
分析:
# GC 日誌分析
Application time: 900.000 seconds
GC time: 100.000 seconds
Throughput: 90%調校方案:
# 使用 Parallel GC 追求高吞吐量
-XX:+UseParallelGC \
-XX:+UseParallelOldGC \
-XX:ParallelGCThreads=8 \
-XX:GCTimeRatio=19 \ # 目標吞吐量 95%
-XX:MaxGCPauseMillis=500 \ # 可以接受較長暫停
-Xms16g -Xmx16g最佳化結果:
- 吞吐量提升到 97%
- 批次處理時間縮短 20%
容器環境調校#
Docker/Kubernetes 注意事項#
# Java 10+ 自動識別容器限制
-XX:+UseContainerSupport
# 設定容器記憶體佔比
-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=75.0
# 或明確設定
-Xmx$(( ${MEMORY_LIMIT} * 75 / 100 ))m容器中的 Java 8u131 之前版本無法正確識別容器記憶體限制,會使用宿主機記憶體計算堆積大小,可能導致 OOM Killer 殺死程序。
容器推薦組態#
# Dockerfile 範例
FROM openjdk:17-slim
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof \
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]總結#
JVM 調校的核心要點:
- 方法論:監控 → 分析 → 定位 → 調校 → 驗證
- 工具使用:
- 命令列:jps、jstat、jmap、jstack、jcmd
- 圖形化:JConsole、VisualVM、MAT
- 日誌分析:GCViewer、GCEasy
- 常見問題:
- 頻繁 Full GC:增加記憶體、調整分代比例
- 記憶體洩漏:MAT 分析堆積傾印
- CPU 過高:執行緒分析、Profiling
- 環境適配:容器環境需要特別組態
- 持續最佳化:調校是持續的過程,需要不斷監控和改進