5.3 Finalization and garbage collection
- 程式設計師需意識到清理資源的重要性,尤其是使用了非標準記憶體分配(如通過JNI分配的本地記憶體)的物件來說,簡單的放手可能並不安全。對於基本資料類型如整數,通常不需要清理。
- Java 有垃圾收集機制來回收不再使用的 object 記憶體,但若 object 分配了「特殊」記憶體,則垃圾收集器可能無法處理
- Java 的
finalize()
方法允許在物件記憶體回收前執行清理,但與 C++ 的 destructor 函數不同,finalize()
並不保證一定會被執行,因其執行依賴於垃圾收集的觸發 - 若物件操作涉及需要結束前進行的特定清理工作,應該手動實現,不能依賴自動垃圾收集,因為:
- object 可能不會被垃圾回收
- 垃圾回收並不等於 destructor
- 例如: 在螢幕繪製的應用中,透過 C++ 的 destructor 可以在物件銷毀時確保進行必要的資源回收,而在 Java 中,如果記憶體一直充足,該物件可能在程式結束前永遠不會被回收
finalize 的用途為何
- 這邊要記住的重點: 垃圾回收只與 Java 虛擬機管理的 heap 記憶體有關
- 無論 object 如何被建立,都由垃圾回收器來負責釋放該 object 的記憶體
finalize()
的用途限於特殊情況: 用於那些以非標準方式分配記憶體的情況,尤其是涉及非 Java 代碼的情況- 在非 Java 代碼中有可能調用 C 語言的
malloc()
來分配記憶體。這種情況下,除非 適當地調用free()
,否則記憶體將無法被釋放,造成 memory leak - 然而,由於
free()
是 C/C++ 的函數,需要在finalize()
中通過 JNI 來調用,以確保這部分非標準記憶體得以正確釋放
- 在非 Java 代碼中有可能調用 C 語言的
必須由你實作清理
- 在 Java 中,所有物件的清理需要使用者在需要時顯式調用清理方法。這與 C++ 中的 destructor 函數概念有所衝突,後者會在物件離開創建它的作用域時自動觸發銷毀
- 在 C++ 中,位於 stack 上的本地物件會在作用域結束時自動銷毀。相比之下,Java 中沒有 local object 的概念;所有物件都在 heap 上通過
new
關鍵字創建- 在 C++ 中,如果忘記調用
delete
,相應的 destructor 函數也不會執行,導致記憶體洩漏 - 相對地,在 Java 中,物件的釋放完全依賴於垃圾收集器的介入
- 在 C++ 中,如果忘記調用
- 雖然 Java 擁有垃圾收集機制,但這不意味著不需要進行類似於 C++ destructor 函數的清理工作。若需進行除記憶體釋放以外的清理,則必須在 Java 中明確調用適當的清理方法,雖然這缺乏 C++ 中自動銷毀的便利性
- 垃圾收集和
finalize()
方法的執行都沒有保證:- 如果 Java 虛擬機 (JVM) 沒有遇到記憶體壓力,則可能不會觸發垃圾回收,從而不回收記憶體
- 對於需要特定清理的操作,不應僅依賴這些自動化機制
終結條件 (The termination condition)
finalize()
有個有趣的用法: 對 object 終結條件(termination condition) 的驗證- 當你不再需要一個物件時,該物件應該處於一個可以安全釋放其記憶體的狀態。例如,如果一個物件代表一個開啟的檔案,則該檔案應在物件被垃圾回收前被關閉
- 如果物件的某些部分未被適當清理,這可能表明你的程式中存在錯誤。即使
finalize()
不保證總是被執行,它仍可以用來揭露這種錯誤
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
protected void finalize() {
if(checkedOut)
System.out.println("Error: checked out");
// Normally, you'll also do this:
// super.finalize(); // Call the base-class version
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Proper cleanup:
novel.checkIn();
// Drop the reference, forget to clean up:
new Book(true);
// Force garbage collection & finalization:
System.gc();
}
} /* Output:
Error: checked out
*/
- 所有的
Book
object 在被當成垃圾回收前都應該被 check in,但這個例子中有一本書未被 check in,如果沒有finalize()
來驗證,這個缺陷可能難以發現
備註
System.gc()
是 Java 的一個方法,用來建議虛擬機(JVM)執行垃圾收集(GC)。重要的是要明白,System.gc()
並不保證垃圾收集器會立即執行,它僅是向 JVM 提出請求,希望能夠進行垃圾收集- 即便不使用
System.gc()
,如果程序多次執行並分配大量記憶體,導致垃圾回收,最終錯誤的Book
物件也會被發現。這是因為垃圾收集器的設計旨在回收不再使用的記憶體,從而提高記憶體使用效率
垃圾回收器如何工作
- 在 Java 中,所有 object(除了 primitives)都在 heap 上分配,這可能看起來是一個昂貴的操作。但是,由於垃圾收集器的存在,實際上這種分配方式可以非常快 速
- 某些 JVM 實現了類似“傳送帶”的 heap 模型,每次新對象分配時,只需將 heap 指針簡單地向前移動到未使用的區域,這使得對象存儲分配迅速得多
- 一種簡單但較慢的垃圾收集技術是引用計數(reference counting),其中每個對象包含一個引用計數器,引用連接或解除時計數器變化。然而,由於循環引用問題,這種方法並未在 JVM 中使用
- 較快的垃圾收集方法,不使用引用計數,而是基於核心假設:所有未死亡的 object 都必須最終能夠從 stack 或靜態存儲中的 reference 追踪到
- 收集過程從 stack 和靜態存儲區域的引用作為活躍對象的 root 開始
- 垃圾收集器遍歷所有引用鏈,對於每一個找到的引用,進行遞迴追蹤,以確保遍歷到的每個 object 都是活躍的
- 在追踪過程中,可以找出哪些 object 是活躍的(被引用的),哪些是垃圾(未被引用的)
- 不會因循環引用而失效
- Java 虛擬機 (JVM) 採用適應性(adaptive)垃圾收集機制,在這種方式下,垃圾收集策略會根據運行條件自動調整:
- Stop-and-Copy:
- 此策略在執行時會暫停程序,將所有活躍的對象從一個 heap 轉移到另一個新的 heap 中。這種方法雖然能提高存儲效率,但可能在記憶體使用上較為低效,尤其是對於生成較少垃圾的程序來說,因為它需要維護兩個堆並在它們之間不斷移動記憶體
- Mark-and-Sweep:
- 此法透過標記所有活躍對象,然後清掃掉未被標記的對象(即垃圾),來釋放記憶體。這種方法不涉及對象的物理移動,因此更適合垃圾較少的情況。當程序生成的垃圾較少時,此法比 Stop-and-Copy 方法更高效,因為它避免了不必要的記憶體移動
- JVM 會根據垃圾生成的情況和 heap 的碎片化程度在這兩種方法之間切換,以優化記憶體管理和程序性能。這種適應性策略使得 JVM 能在不同條件下選擇最適合的垃圾收集方法
- 塊分配:
- JVM 在記憶體中分配大塊(blocks),用於存放大型對象。這種方法不僅幫助管理記憶體更有效,也使得活躍對象能在垃圾收集時從一個堆複製到已死亡的塊中,進一步減少了記憶體碎片
- generation count:
- 每個記憶體塊都有一個代計數,用以追蹤該塊是否仍在使用中。通常,只有自上次垃圾收集後新創建的塊會被壓縮,而其他塊則在被引用時更新其代計數
- 塊分配:
- 垃圾收集策略的適應性切換:
- JVM 定期進行全面掃描,大型對象不會被複製,只更新其代計數;而包含小對象的塊則被複製和壓縮
- JVM 監控垃圾收集的效率,如果因為 object 壽命長導致收集成本過高,則切換到標記-清掃(mark-and-sweep)方法
- 如果使用標記-清掃方法後 heap 開始碎片化,則再次切換回停止-複製(stop-and-copy)方法
- Stop-and-Copy:
- Just-In-Time (JIT) 編譯器
- Just-In-Time (JIT) 編譯器是一種強大的技術,它將 Java 程序的部分或全部轉換成本機機器碼,進而不需要在 JVM 中完全依賴於解釋執行,從而顯著提高程序的運行速度
- 當 JVM 加載一個 class 時,如需創建該 class 的 object,相應的 .class 文件被定位並加載到記憶體中。這時,可以選擇對該 class 的代碼進行 JIT 編譯,優先編譯最頻繁執行的代碼段,以提高執行效率
- 缺陷
- 全面 JIT 編譯雖然能提高運行時的效率,但會增加程序的啟動時間並擴 大可執行文件的大小,這可能引起分頁(paging),進而減慢程序速度
- 替代策略:採用延遲評估(lazy evaluation)。即只在碼段首次執行時進行 JIT 編譯,從而避免了從未執行的代碼被無謂地編譯。這種策略有助於減少啟動延遲,並降低不必要的資源消耗
- Just-In-Time (JIT) 編譯器是一種強大的技術,它將 Java 程序的部分或全部轉換成本機機器碼,進而不需要在 JVM 中完全依賴於解釋執行,從而顯著提高程序的運行速度