Java 重要優點之一是透過垃圾收集器 (Garbage Collection, GC) 自動管理記憶體回收,程式設計師無需呼叫函式釋放記憶體。許多人因此認為 Java 不存在記憶體洩漏,或認為即使有也是 GC 或 JVM 的責任。其實這想法不正確,Java 也存在記憶體洩漏,只是表現與 C++ 不同。
隨著伺服器程式大量採用 Java 技術 (JSP、Servlet、EJB 等),長期運行下,甚至嵌入式系統記憶體有限,記憶體洩漏問題變得關鍵。即使每次洩漏極少,長期運行後系統也面臨崩潰風險。
Java 記憶體管理即物件分配與釋放。程式設計師透過 new 關鍵字為每個物件申請記憶體空間 (基本型別除外),所有物件在堆疊 (Heap) 中分配。物件釋放由 GC 決定執行。記憶體分配由程式完成,釋放由 GC 完成,這種收支兩條線的方法簡化了程式設計師工作,但加重了 JVM 負擔,這也是 Java 程式執行速度較慢的原因之一。GC 為正確釋放物件,必須監控每個物件的運行狀態:申請、引用、被引用、賦值等。
監控物件狀態是為更準確、及時地釋放物件,根本原則是該物件不再被引用。可將物件視為有向圖的頂點,引用關係為有向邊,從引用者指向被引用物件。每個執行緒物件可作為圖的起始頂點 (如 main 執行緒)。在有向圖中,根頂點可達的物件為有效物件,GC 不回收。若某物件 (連通子圖) 與根頂點不可達,視為不再被引用,可被 GC 回收。
Java 使用有向圖管理記憶體,可消除循環引用問題 (三個物件相互引用,若與根不可達,GC 仍可回收)。優點是管理精度高,缺點效率較低。另一常用技術是計數器 (如 COM 模型),精度較低 (難處理循環引用) 但執行效率高。
Java 記憶體洩漏指存在已分配的物件,具備兩個特徵:1) 可達 (有向圖中存在路徑相連);2) 無用 (程式以後不會再使用)。滿足這兩條件即判定為記憶體洩漏,這些物件不會被 GC 回收,卻佔用記憶體。
C++ 中記憶體洩漏範圍較大:有些物件分配記憶體後不可達,因無 GC 將永遠回收不了。Java 中不可達物件由 GC 負責,程式設計師無需考慮這部分。對 C++ 程式設計師需自己管理邊與頂點,Java 只需管理邊 (不需管理頂點釋放),提高編程效率。Java 從語言層面保證任何物件皆可達,所有不可達物件由 GC 管理。
GC 基本對程式設計師透明、不可見。雖有 System.gc() 等函數可存取 GC,但 Java 語言規範定義該函數不保證 JVM 垃圾收集器一定執行。不同 JVM 實作可能用不同演算法管理 GC。通常 GC 執行緒優先級較低。JVM 呼叫 GC 策略多種:記憶體使用達一定程度、定時執行、平緩執行、分代執行。通常無需關心,除非特定場合 GC 影響效能 (如 Web 即時系統、網路遊戲),需調整 GC 參數讓 GC 以平緩方式釋放記憶體 (將垃圾回收分解為一系列小步驟),Sun HotSpot JVM 支援此特性。