Thinking in Java(4-3) 深入探討 Java 中的 finalize() 方法與垃圾回收機制

Thinking in Java(4-3) 深入探討 Java 中的 finalize() 方法與垃圾回收機制
Photo by orbtal media / Unsplash

在 Java 程式設計中,資源的清理和管理是不可忽視的關鍵部分。特別是當我們處理非標準記憶體分配(例如透過 JNI 分配的本地記憶體)時,僅僅依賴自動垃圾回收可能並不安全。本篇文章將深入探討 finalize() 方法的用途與限制,以及垃圾回收器的運作機制。

為何需要關注資源清理

程式設計師應該意識到,對資源的適當清理對於維持應用程式的穩定性和性能至關重要。雖然對於基本資料型態(如整數)通常不需要特別的清理,但當涉及到物件(尤其是那些使用非標準記憶體分配的物件)時,必須確保它們在不再使用時被正確地回收。

Java 提供了自動垃圾回收機制,負責回收不再使用的物件記憶體。然而,這個機制主要針對 JVM 管理的堆積(heap)記憶體,對於透過 JNI 分配的本地記憶體等「特殊」記憶體,垃圾回收器可能無法處理。

finalize() 方法的用途與限制

什麼是 finalize() 方法

finalize() 方法允許我們在物件被垃圾回收之前執行特定的清理操作。然而,與 C++ 中的解構子(destructor)不同,finalize() 並不保證一定會被執行,因為它的執行取決於垃圾回收的觸發。

finalize() 的適用情況

  • 非標準記憶體清理finalize() 的主要用途是處理以非標準方式分配的記憶體,特別是涉及非 Java 程式碼的情況。例如,在本地程式碼中可能會使用 C 的 malloc() 來分配記憶體,這些記憶體不會由垃圾回收器自動回收。
  • 透過 JNI 釋放本地記憶體:為了確保這些非標準記憶體被正確釋放,需要在 finalize() 方法中透過 JNI 調用 C 的 free() 函數。

為何不能完全依賴 finalize()

  • 執行時間不確定finalize() 方法的執行時間不確定,因為垃圾回收的觸發時機由 JVM 決定。
  • 可能不被執行:如果 JVM 沒有遇到記憶體壓力,垃圾回收可能永遠不會發生,導致 finalize() 不被執行。
  • 需要手動清理:對於需要在物件生命週期結束前執行的特定清理工作,應該手動實作清理方法,而不能依賴 finalize()

手動實作清理方法的重要性

在 Java 中,所有物件的清理應該由開發者在需要時明確調用,這與 C++ 的自動解構機制不同。在 C++ 中,位於堆疊(stack)上的本地物件會在作用域結束時自動銷毀,但 Java 中所有物件都在堆積(heap)上透過 new 關鍵字創建,沒有自動清理機制。

範例:驗證物件的終結條件

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 物件在被垃圾回收前應該被 checkIn(),否則會在 finalize() 方法中輸出錯誤訊息。

注意System.gc() 只是建議 JVM 進行垃圾回收,不保證立即執行。即使不使用 System.gc(),當系統資源緊張時,垃圾回收器也會自動運作。

垃圾回收器的運作機制

為什麼 Java 的物件分配速度快

  • 堆積分配:Java 中所有物件都在堆積上分配,但由於垃圾回收器的設計,這種分配方式非常快速。
  • 連續分配:一些 JVM 採用了類似「傳送帶」的堆積模型,新物件的分配僅需將指標移動到未使用區域。

垃圾回收的策略

  • 引用計數(Reference Counting):這是一種簡單但較慢的垃圾回收技術,每個物件包含一個計數器,當引用變化時更新計數。但此方法無法處理循環引用,因而未被 JVM 採用。
  • 標記-清除(Mark-and-Sweep):此方法標記所有活躍物件,然後清除未標記的物件。適合垃圾產生較少的情況。
  • 停止-複製(Stop-and-Copy):此策略將所有活躍物件從一個堆積轉移到另一個新的堆積,適合垃圾產生較多的情況。

JVM 的自適應垃圾回收

JVM 採用了自適應的垃圾回收機制,會根據程式的運行情況自動選擇最佳的垃圾回收策略。

  • 塊分配與代數計數:JVM 將記憶體劃分為塊,並使用代數計數追蹤物件的存活時間。
  • 策略切換:JVM 會根據垃圾產生和堆積碎片化的程度,在標記-清除和停止-複製之間切換。

Just-In-Time (JIT) 編譯器的影響

什麼是 JIT 編譯器

JIT 編譯器是一種強大的技術,能將 Java 程式的部分或全部轉換為本機機器碼,從而提高執行效率。

JIT 編譯器的優勢與缺點

  • 優勢:顯著提升程式的運行速度,特別是對頻繁執行的代碼段。
  • 缺點:可能增加程式的啟動時間和記憶體佔用,因為需要編譯更多的代碼。

延遲評估策略

為了平衡效率和資源佔用,JIT 編譯器通常採用延遲評估(Lazy Evaluation)的策略,即僅在代碼首次執行時才進行編譯,避免不必要的編譯工作。

結論

在 Java 中,理解 finalize() 方法的限制和垃圾回收器的運作機制,有助於我們更有效地管理資源。雖然 Java 提供了自動垃圾回收,但對於特殊資源的清理,仍需我們手動實作。此外,熟悉 JIT 編譯器的工作原理,也能幫助我們寫出性能更佳的程式。