同事反饋一段代碼執行極慢,CPU 長期 100%。經歷「優化計算邏輯 → 引入多線程 → 仍無效」的彎路後,最終通過擴大堆內存驗證假設,定位到頻繁 Full GC 導致業務線程饑餓。本文完整復盤定位思路、根因分析與 JVM 內存參數知識,助你避開「先入為主」的調優陷阱。
// 核心業務:400萬 × 96 次循環
HashMultimap map = ...;
for (int i = 0; i < 400_0000 * 96; i++) {
Bean bean = doCompute(); // 計算耗時
map.put(key, bean); // 緩存結果
}
初步嘗試:優化 doCompute()、引入 ConcurrentLinkedQueue 解耦生產消費、多線程並行 → CPU 依然 100%,吞吐無提升。
下班前靈光一試:給服務器分配 20G 堆,零修改代碼,直接跑通,CPU 正常。這一驗證瞬間推翻「計算慢」假設,鎖定內存壓力 → GC 暴走。
HashMultimap + ConcurrentLinkedQueue 每次循環新建大量 Bean、Node、Map.Entry 等短命對象map 持有所有結果引用,對象全是存活的,GC 掃描整個堆卻回收接近零-Xmx 上限,只是在「分配 → GC → 分配 → GC」死循環中耗盡 CPU| 參數 | 默認值 | 觸發條件 | 調整建議 |
|---|---|---|---|
-Xms | 物理內存 1/64 | 初始堆大小 | 生產環境建議 -Xms = -Xmx 避免擴容抖動 |
-Xmx | 物理內存 1/4 | 最大堆大小 | 預留 30% 給 Off-Heap、Metaspace、OS |
-XX:MinHeapFreeRatio=40 | 40% | 空閒 < 40% 觸發擴容 | 大內存應用可調大減少擴容頻率 |
-XX:MaxHeapFreeRatio=70 | 70% | 空閒 > 70% 觸發縮容 | 固定堆時無效 |
注意:free 命令顯示的可用內存 ≠ JVM 可分配內存,受 OS 頁面緩存、共享內存、碎片化影響。
| 方案 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 增大堆(-Xmx) | 推遲 GC 觸發點 | 零代碼改動;立竿見影 | 治標不治本;大堆 Full GC 停頓更長 | 臨時緩解、數據量有上限 |
| 分批處理 + 定期清理 | 分段寫入、中間落盤/推流 | 內存佔用可控;流式處理 | 需改造業務邏輯;引入中間件 | 海量數據持久化/流計算 |
| 對象復用/對象池 | 減少新建對象 | 降低 GC 壓力源頭 | 增加代碼複雜度;線程安全難 | 高頻小對象場景 |
| 換用 G1/ZGC | 區域化/並發標記壓縮 | 大堆低延遲 | JDK 版本要求高;調優參數多 | JDK 11+、堆 > 8G |
-Xms -Xmx 設大一點跑跑看,成本最低、區分度最高jstat -gcutil、jmap -histo、GC 日志 -Xlog:gc*、Arthas、JProfiler下周計劃補上 -Xlog:gc*:file=gc.log 實測日誌驗證本文分析。記住:性能調優的本質是「找到系統真正的瓶頸在哪裡」,而不是「優化你以為的瓶頸」。