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 -m

jstat - 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, S1Survivor 0/1 使用率
EEden 使用率
OOld 老年代使用率
MMetaspace 使用率
CCS壓縮類別空間使用率
YGCYoung GC 次數
YGCTYoung GC 總耗時(秒)
FGCFull GC 次數
FGCTFull GC 總耗時(秒)
GCTGC 總耗時

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 -> Plugins

VisualVM 功能:

  • 即時監控 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,tags

GC 日誌分析工具#

GCViewer#

# 下載並執行
java -jar gcviewer-1.36.jar gc.log

GCEasy(線上工具)#

上傳 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 GC
  • Using 4 workers:使用 4 個 GC 執行緒
  • 388M->190M(512M):GC 前 388MB → GC 後 190MB,堆積總大小 512MB
  • 5.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=256m

GC 設定#

# 選擇 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();
// 忘記關閉 conn

3. 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 調校的核心要點:

  1. 方法論:監控 → 分析 → 定位 → 調校 → 驗證
  2. 工具使用
    • 命令列:jps、jstat、jmap、jstack、jcmd
    • 圖形化:JConsole、VisualVM、MAT
    • 日誌分析:GCViewer、GCEasy
  3. 常見問題
    • 頻繁 Full GC:增加記憶體、調整分代比例
    • 記憶體洩漏:MAT 分析堆積傾印
    • CPU 過高:執行緒分析、Profiling
  4. 環境適配:容器環境需要特別組態
  5. 持續最佳化:調校是持續的過程,需要不斷監控和改進