一、垃圾回收的意義與權衡
在 C/C++ 中,對象佔用的內存需手動釋放;Java 引入 GC 自動管理內存:當無引用指向對象時,JVM 系統線程自動回收其佔用空間。GC 不僅釋放無用對象,還能整理內存碎片(將存活對象移至堆一端,騰出連續空間)。
優點:
- 提高編程效率:避免手動內存管理的複雜錯誤
- 保護程序完整性:是 Java 安全策略核心部分
潛在缺點:
- 性能開銷:追蹤有用對象、釋放無用對象消耗 CPU 時間
- 早期算法不完備:無法 100% 回收所有廢棄內存(現代算法已大幅改善)
二、垃圾收集算法演進
所有 GC 算法核心做兩件事:發現無用對象、回收其佔用內存。主流算法均基於根集概念:正在執行的 Java 程序可訪問的引用變量集合(局部變量、參數、類變量)。
1. 引用計數法
- 每個對象有引用計數器,賦值加 1,離開作用域減 1,歸零即可回收
- 優點:執行快、不長時間中斷程序、適合實時系統
- 缺點:增加程序執行開銷;無法處理循環引用
2. 追蹤算法 / 標記-清除
- 從根集掃描,標記可達對象,清除不可達對象
- 解決了循環引用問題
3. 壓縮算法
- 清除過程中將所有對象移至堆一端,形成連續空閒區
- 需更新所有引用指向新位置,通常引入句柄表間接訪問
4. 複制算法
- 堆分為對象區和空閒區,GC 時將活動對象複製到空閒區,角色互換
- 典型:Stop-and-copy(切換過程中程序暫停)
- 優點:無碎片、分配簡單(指針碰撞)
- 缺點:浪費一半空間;需複製所有存活對象
5. 分代算法(現代商用 JVM 主流)
- 堆分為新生代、老年代(甚至更多代)
- 新生代:對象朝生夕死 → 複制算法(僅複製少量存活對象)
- 老年代:對象存活率高 → 標記-清除/整理
- 優點:針對不同代特點選擇最優算法,大幅提升效率
6. 自適應算法
三、System.gc() 與命令行觀察
System.gc():僅是建議,JVM 不保證立即執行,只會增加 GC 發生概率
-verbose:gc:啟動時輸出 GC 日誌,格式如 [Full GC 168K->97K(1984K), 0.0253873 secs]
- 箭頭前後:GC 前後存活對象佔用內存
- 括號內:堆總容量
- 時間:GC 耗時
四、finalize() 方法深度剖析
protected void finalize() throws Throwable
- 對象被 GC 回收前調用,用於釋放非 Java 堆內存資源(如 JNI 分配的內存、文件句柄、數據庫連接)
- 調用時機不確定,不建議依賴它釋放關鍵資源
- 優先使用
try-with-resources、AutoCloseable、Cleaner(Java 9+)
- 任何對象
finalize() 只會被調用一次
五、主 GC 觸發條件
- 應用程序空閒時: GC 線程優先級最低,僅在無用戶線程運行時執行
- Java 堆內存不足時: 創建新對象空間不足 → 強制觸發 GC → 若仍不足再試兩次 → 拋出
OutOfMemoryError
主 GC 具有不確定性,長期運行應用會反覆觸發。
六、減少 GC 開銷的工程實踐
| 措施 | 原理 | 代碼示例 |
| 不顯式調用 System.gc() | 避免增加 Full GC 頻率 | — |
| 減少臨時對象創建 | 減少垃圾產生,延長 Minor GC 間隔 | 循環外複用對象、使用 StringBuilder |
| 對象不用時置 null | 幫助 GC 更快識別垃圾 | list = null; map.clear(); |
| 字符串拼接用 StringBuilder | 避免產生大量中間 String 對象 | new StringBuilder().append(a).append(b) |
| 優先用基本類型 | int 比 Integer 佔用內存少得多 | int count vs Integer count |
| 少用靜態變量 | 靜態變量生命周期長,不易被回收 | — |
| 分散對象創建/刪除時間 | 避免瞬間大量分配觸發 Full GC | 批量處理改為流式/分批 |
七、GC 核心特點與最佳實踐總結
- 不可預知性: 不要假定 GC 發生時間
- 精確性: 精確標記存活對象、精確定位引用關係,防止內存洩漏與碎片
- 多樣性: 多種收集器並存,有 STW、並發、並行等不同模式
- JVM 耦合性: 實現與具體 JVM、內存模型緊密相關
- 可配置性: 現代 GC 提供豐富參數,可根據應用環境調優
開發者注意事項:
- 不要假定 GC 時間
System.gc() 不保證執行
- 選擇適合的收集器(無特殊需求用默認;實時系統考慮增量/並行;高配服務器考慮並行標記/清除)
- 防止內存洩漏是關鍵:良好編程習慣、及時釋放引用、去除監聽器
- 盡早釋放無用引用:離開作用域自動置 null,注意移除監聽器後再置空
參考:《深入理解 Java 虛擬機》