| 條件 | 說明 | 影響 |
| 1. 老年代空間不足 | 對象晋升失敗、大對象直接進老年代無空間 | 嚴重停頓,核心排查目標 |
| 2. 永久代/元空間不足 (JDK 7/8+) | 類加載過多、類卸載失效 | 類卸載導致應用異常 |
| 3. 平均晋升大小 > 老年代剩餘空間 | 動態年齡判斷、大對象晋升預判失敗 | 預防性 Full GC |
| 4. 顯式調用 System.gc() | RMI 定時觸發、代碼誤調用 | 可通過 -XX:+DisableExplicitGC 禁用 |
| 5. CMS 並發模式失敗 | 浮動垃圾過多、碎片導致分配失敗 | 退化為 Serial Old 全堆掃描 |
| 6. 統計得到的 Minor GC 晉升平均大小 > 老年代剩餘 | 基於歷史統計的預防性觸發 | HandlePromotionFailure 設置影響 |
關鍵認知:Full GC 由 JVM 根據運行時狀態動態決定,具有不確定性,長期運行應用必然反覆發生 Full GC。
二、減少 GC 開銷的 5 大編程技巧
技巧 1:避免隱式 String 拼接產生大量臨時對象
問題代碼:
String result = foo() + arg;
result += boo();
System.out.println("result = " + result);
編譯器生成的等價代碼(每個 + 產生一個 StringBuilder + 兩個 String):
StringBuilder temp1 = new StringBuilder(foo()).append(arg);
String result = temp1.toString(); // 新 String
StringBuilder temp2 = new StringBuilder(result).append(boo());
result = temp2.toString(); // 新 String
StringBuilder temp3 = new StringBuilder("result = ").append(result);
System.out.println(temp3.toString()); // 新 String
優化方案:顯式使用單個 StringBuilder:
StringBuilder sb = new StringBuilder("result = ");
sb.append(foo()).append(arg).append(boo());
System.out.println(sb);
效果:從 5 個臨時對象減少到 1 個 StringBuilder + 1 個結果 String。
技巧 2:預設集合初始容量,避免擴容拷貝
問題:ArrayList 默認容量 10,擴容 1.5 倍涉及數組拷貝、舊數組變垃圾。
// 低效
List- items = new ArrayList<>();
for (int i = 0; i < len; i++) items.add(readNextItem());
// 高效:預估大小
List
- items = new ArrayList<>(len); // 或 len * 1.2 預留緩衝
for (int i = 0; i < len; i++) items.add(readNextItem());
同理適用於 HashMap、HashSet、StringBuilder 等。
技巧 3:使用基本類型專用集合,消除裝箱開銷
痛點:Map 每次 put 都裝箱 Integer,產生臨時對象;擴容時 Entry 內部對象再次分配。
方案:引入 Trove4j、FastUtil、Eclipse Collections:
// Trove 示例
TIntObjectHashMap map = new TIntObjectHashMap<>();
map.put(userId, user); // 無裝箱,底層基本類型數組
收益:大幅減少 GC 壓力,提升緩存命中率,JDK 10+ 將引入 Value Types 徹底解決。
技巧 4:流式處理替換內存緩衝,避免大對象駐留
反模式:讀取大文件/網絡流 → ByteArrayOutputStream 緩衝全量 → 反序列化
正模式:直接對 InputStream 反序列化
// Jackson 流式解析
ObjectMapper mapper = new ObjectMapper();
try (InputStream is = new FileInputStream("big.json")) {
MyObject obj = mapper.readValue(is, MyObject.class);
}
原理:持久化庫 (Jackson/Protobuf/Kryo) 直接從流讀取,無需中間 byte[] 緩衝。
技巧 5:傳入集合參數替代返回新集合,減少中間對象
低效模式:每次調用創建新 List 合併
List- items = new ArrayList<>();
for (FileData fd : fileDatas) {
items.addAll(readFileItem(fd)); // 每次調用產生臨時 List + 內部數組
}
高效模式:複用單一集合
List- items = new ArrayList<>(estimatedTotalSize);
for (FileData fd : fileDatas) {
readFileItem(fd, items); // 直接往傳入集合添加
}
權衡:違反不可變性原則,但在高頻熱點路徑上,對 GC 的緩解效果顯著。
三、減少 GC 開銷的 7 條通用措施
- 不要顯式調用
System.gc():僅建議 JVM,可能觸發不必要的 Full GC,增加停頓
- 減少臨時對象創建:縮短對象生命週期,延後 Eden 滿觸發 Minor GC
- 對象不用盡早置
null:幫助 GC 識別垃圾,特別是大對象、集合引用
- 字符串拼接用
StringBuilder:避免產生大量中間 String 對象
- 優先基本類型 (
int/long) 而非包裝類:節省對象頭開銷,避免裝箱拆箱
- 慎用靜態集合/對象:生命週期隨類加載器,易造成內存洩漏
- 分散對象創建/銷毀時間:避免瞬間大量分配觸發 Full GC、瞬間大量釋放導致碎片整理
四、垃圾回收關鍵特性與實踐建議
| 特性 | 含義 | 實踐啟示 |
| 觸發時間不可預知 | 取決於算法、參數、運行狀態 | 不要假設對象何時被回收 |
| 精確性 | 精確標記存活對象、精確定位引用 | 無內存洩漏前提、支持壓縮整理 |
| 多樣化收集器 | 串行/並行/並發、單/多線程 | 根據業務選型:吞吐量優先→並行;延遲敏感→並發 |
| 與 JVM 內存模型緊耦合 | 不同 JVM 實現不同 | HotSpot 為主流,掌握其分代模型即覆蓋 95% 場景 |
| 可配置參數豐富 | 每種收集器可調參數 | 根據應用特徵精細調優 |
五、避坑指南
- 不要嘗試預測 GC 時間:方法結束的局部變量不一定立即回收
System.gc() 不保證執行:僅發起請求,JVM 可忽略
- 選擇合適收集器:無特殊要求用默認;實時系統→增量/並發;大吞吐→並行標記清除
- 內存洩漏是核心難點:良好編程習慣 > 任何工具
- 盡早釋放無用引用:退出作用域、移除監聽器、清空集合、置 null
六、參考資源
七、總結
GC 優化的本質是源頭減少垃圾產生而非依賴調優參數補救。掌握 5 大編程技巧(StringBuilder、預設容量、基本類型集合、流式處理、集合複用)配合 7 條通用措施,能從代碼層面構建「GC 友好型」應用,顯著降低停頓、提升吞吐、延緩 Full GC 到來。
- 上一篇文章: Java 字符串轉 JSON 數組並遍歷:基於 net.sf.json 實戰指南
- 下一篇文章: 《編寫高質量代碼》第 7 章精讀:泛型與反射最佳實踐 (建議 102-105)
Copyright ©
程式碼之家 All Rights Reserved