內存分配/回收佔用應用 CPU 的 10-30%,GC 停頓 (Stop-the-World) 直接決定尾部延遲。Java 5 (JDK 1.5) 引入人因工程學——自適應擇優策略,並提供三款收集器滿足不同 SLA。本文深度解析原理、參數、調優實戰與避坑指南。
| 收集器 | 啟動參數 | 模式 | 目標場景 | 核心特征 |
|---|---|---|---|---|
| Serial GC | -XX:+UseSerialGC | 單線程、串行 | Client 模式、單核、嵌入式、小堆 (<100MB) | 無線程切換開銷、STW 長、簡單可預測 |
| Parallel GC (Throughput) | -XX:+UseParallelGC (JDK 8+ 默認) | 多線程並行、STW | 批處理、科學計算、後台任務、吞吐量優先 | 新生代/老年代並行回收、可設目標停頓/吞吐量、自適應調整 |
| CMS (Concurrent Mark Sweep) | -XX:+UseConcMarkSweepGC | 老年代並發標記/清掃、新生代並行 | Web 服務、API 网关、低延遲優先、堆 < 4-6GB | 最小化 STW、碎片整理需 Full GC、浮動垃圾、CPU 敏感 |
| G1 (Garbage First) | -XX:+UseG1GC (JDK 9+ 默認) | 分區、混合、可預測停頓 | 大堆 (6GB-32GB)、混合負載、平衡吞吐/延遲 | Region 劃分、優先回收垃圾多區、Remembered Set、SATB、雙階段標記 |
註:Java 5 僅有前三款,G1 從 Java 6 Update 14 引入,Java 9 成熟轉正。本文聚焦 Java 5 環境 (Parallel GC / CMS)。
| 參數 | 含義 | 建議值/策略 |
|---|---|---|
| -Xms / -Xmx | 堆初始/最大值 | 生產環境 設為相等,避免動態擴縮容抖動。容器環境設 70-80% 物理內存。 |
| -XX:NewRatio=N | 老年代:新生代 比例 | 默認 2 (1:2)。短命對象多 → 降低 (如 1:3/1:4)。大對象多 → 提高。 |
| -XX:SurvivorRatio=N | Eden:Survivor 比例 | 默認 8 (8:1:1)。大批量短命對象 → 降低 (如 4:1:1),減少過早晋升。 |
| -XX:MaxTenuringThreshold=N | 晋升老年代年齡閾值 | 默認 15 (CMS 6)。動態年齡判斷 (-XX:+TargetSurvivorRatio) 通常更優。 |
| -XX:ParallelGCThreads=N | Parallel GC 線程數 | CPU 核心數 ≤ 8 時 = 核心數;>8 時 = 3 + 5/8 * 核心數。容器環境受 cgroups 限制需顯式設定。 |
| -XX:MaxGCPauseMillis=N | 目標最大停頓 (Parallel GC 提示) | 吞吐量優先場景勿設或設大 (如 500ms);低延遲場景 CMS/G1 更適合。 |
| -XX:GCTimeRatio=N | 目標吞吐量比 = 1/(1+N) | 默認 99 (1% GC 時間)。批處理可設 19 (5% GC 時間) 換取更大吞吐。 |
| -XX:+UseAdaptiveSizePolicy | 自適應調整新生代/老年代/ Survivor 大小 | Parallel GC 默認開啟,配合目標停頓/吞吐量自動調優。CMS 不支持。 |
| -XX:+UseConcMarkSweepGC | 啟用 CMS | 配合 -XX:+UseParNewGC (新生代並行) 使用。 |
| -XX:CMSInitiatingOccupancyFraction=N | 老年代佔用觸發 CMS 閾值 | 默認 ~68% (Java 6+ 92%)。提前觸發 (如 60-70%) 留足並發標記時間,防止 Concurrent Mode Failure 退化 Full GC。 |
| -XX:+UseCMSInitiatingOccupancyOnly | 僅使用上述閾值觸發 | 建議開啟,禁用自適應觸發邏輯,行為可預測。 |
| -XX:+CMSIncrementalMode | 增量式 CMS (i-CMS) | 單核/雙核機器上將並發工作切片穿插 Minor GC 間隙。多核無需開啟。 |
| -XX:+CMSClassUnloadingEnabled | 允許永久代類卸載 | 動態類加載場景 (OSGi/JSP 熱部署) 必開,防 PermGen OOM。 |
| -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log | GC 日誌輸出 | 生產必開,日誌輪轉 (-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M)。 |
# Parallel GC Minor GC
[GC [PSYoungGen: 325407K->83000K(776768K)] 325407K->83000K(1000000K), 0.2300771 secs]
[Times: user=0.45 sys=0.01, real=0.23 secs]
# Parallel GC Full GC
[Full GC [PSYoungGen: 83000K->0K(776768K)] [ParOldGen: 184628K->83769K(223232K)]
267628K->83769K(1000000K), 1.8479984 secs]
[Times: user=3.20 sys=0.05, real=1.85 secs]
# CMS 並發階段 (無 STW)
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.124/0.124 secs] [Times: user=0.25 sys=0.00, real=0.12 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.045/0.045 secs] [Times: user=0.08 sys=0.00, real=0.04 secs]
# CMS 退化 Full GC (災難)
[Full GC [CMS: 184628K->83769K(223232K), 5.234 secs] 267628K->83769K(1000000K), 5.234 secs]
關鍵指標:Minor GC 頻率/耗時/存活率、Full GC 觸發原因/頻率/耗時、老年代佔用趨勢、CMS 並發階段耗時/失敗次數。
| 誤區 | 後果 | 正確認知 |
|---|---|---|
| 盲目追求「零 Full GC」 | 為此不惜增大堆到 20GB+,導致單次 Full GC 耗時數十秒甚至分鐘 | 接受可控頻率/耗時的 Full GC (如每天 1 次 < 5s),關鍵是業務 SLA 是否達標 |
| 生產環境 -Xms ≠ -Xmx | 運行期動態擴容觸發 Full GC、內存碎片、容器 OOM Kill | 固定堆大小,避免擴縮容抖動 |
| CMS 用於大堆 (>6GB) | 並發標記線程競爭、碎片整理 Full GC 極慢、浮動垃圾佔用大 | 大堆首選 G1 (JDK 9+) 或 ZGC/Shenandoah (JDK 11+) |
| 忽略直接內存/Metaspace | NIO/Netty/DirectByteBuffer/類加載導致堆外 OOM,GC 日誌無反映 | 監控 `-XX:MaxDirectMemorySize`、`-XX:MaxMetaspaceSize`、JMX `NonHeapMemoryUsage` |
| 參數從網上抄襲不驗證 | 業務負載差異巨大,抄來參數反而拖垮性能 | 基於自身 GC 日誌、負載特徵、SLA 目標,單參數迭代驗證 |
GC 調優沒有銀彈,只有負載畫像 + SLA 目標 + 可觀測數據 + 單變量科學實驗。Java 5 時代的 Parallel GC 與 CMS 奠定了「吞吐量優先」與「低延遲優先」兩大調優流派,其核心參數邏輯 (分代結構、自適應調整、並發標記、增量壓縮) 在現代收集器 (G1/ZGC/Shenandoah) 中延續並進化。掌握分代假說、讀懂 GC 日誌、建立基線、科學迭代,是每位 Java 工程師的核心內功。