垃圾收集

C++方法 - 新增和刪除

在像 C++這樣的語言中,應用程式負責管理動態分配的記憶體使用的記憶體。當使用 new 運算子在 C++堆中建立物件時,需要相應地使用 delete 運算子來處理物件:

  • 如果程式忘記了一個物件而只是忘記它,那麼關聯的記憶體就會丟失給應用程式。這種情況的術語是記憶體洩漏,並且記憶體洩漏太多,應用程式可能會使用越來越多的記憶體,並最終崩潰。

  • 另一方面,如果某個應用程式嘗試兩次使用同一個物件,或者在刪除該物件後使用該物件,則該應用程式可能會因記憶體損壞問題而崩潰

在複雜的 C++程式中,使用 newdelete 實現記憶體管理可能非常耗時。實際上,記憶體管理是錯誤的常見來源。

Java 方法 - 垃圾收集

Java 採用不同的方法。Java 提供了一種稱為垃圾收集的自動機制,而不是顯式的 delete 運算子,用於回收不再需要的物件所使用的記憶體。Java 執行時系統負責查詢要處理的物件。此任務由稱為垃圾收集器的元件(簡稱 GC)執行。

在執行 Java 程式的任何時候,我們可以將所有現有物件的集合劃分為兩個不同的子集 1

  • 可訪問物件由 JLS 定義如下:

    可到達物件是可以在任何活動執行緒的任何潛在持續計算中訪問的任何物件。

    在實踐中,這意味著存在從範圍內區域性變數或 static 變數開始的一系列引用,通過這些變數,某些程式碼可能能夠到達該物件。

  • 無法訪問的物件是無法按上述方式到達的物件。

任何無法訪問的物件都有資格進行垃圾回收。這並不意味著他們被垃圾收集。事實上:

  • 無法訪問的物件在無法訪問 1 時 不會立即收集。
  • 無法訪問的物件可能永遠不會被垃圾回收。

Java 語言規範為 JVM 實現提供了很大的自由度,以決定何時收集無法訪問的物件。它(在實踐中)還允許 JVM 實現在檢測無法訪問的物件時保守。

JLS 保證的一件事是,任何可到達的物件都不會被垃圾收集。

當物件變得無法訪問時會發生什麼

首先,當一個物件沒有什麼特別情況變得無法訪問。事情只有在垃圾收集器執行並且檢測到物件無法訪問時才會發生。此外,GC 執行通常不會檢測所有無法訪問的物件。

GC 檢測到無法訪問的物件時,可能會發生以下事件。

  1. 如果有任何 Reference 物件引用該物件,則在刪除物件之前將清除這些引用。

  2. 如果物件是可終結的,那麼它將被最終確定。這在刪除物件之前發生。

  3. 可以刪除該物件,並可以回收它佔用的記憶體。

請注意,有一個明確的序列可以發生上述事件,但沒有什麼要求垃圾收集器在任何特定的時間範圍內執行任何特定物件的最終刪除。

可達和無法訪問的物件的示例

請考慮以下示例類:

// A node in simple "open" linked-list.
public class Node {
    private static int counter = 0;

    public int nodeNumber = ++counter;
    public Node next;
}

public class ListTest {
    public static void main(String[] args) {
        test();                    // M1
        System.out.prinln("Done"); // M2
    }
    
    private static void test() {
        Node n1 = new Node();      // T1
        Node n2 = new Node();      // T2
        Node n3 = new Node();      // T3
        n1.next = n2;              // T4
        n2 = null;                 // T5
        n3 = null;                 // T6
    }
}

讓我們來看看 test() 被呼叫時會發生什麼。語句 T1,T2 和 T3 建立 Node 物件,並且物件都可以分別通過 n1n2n3 變數到達。語句 T4 將第二個 Node 物件的引用分配給第一個 next 物件的 next 欄位。完成後,第二個 Node 可以通過兩條路徑到達:

 n2 -> Node2
 n1 -> Node1, Node1.next -> Node2

在宣告 T5 中,我們將 null 分配給 n2。這打破了 Node2 的第一個可達性鏈,但第二個仍然沒有中斷,所以 Node2 仍然可以到達。

在宣告 T6 中,我們將 null 分配給 n3。這打破了 Node3 的唯一可達性鏈,這使得 Node3 無法到達。但是,Node1Node2 仍可通過 n1 變數訪問。

最後,當 test() 方法返回時,其區域性變數 n1n2n3 超出範圍,因此無法被任何東西訪問。這打破了 Node1Node2 的剩餘可達性鏈,並且所有 Node 物件都不可達,並且有資格進行垃圾收集。

1 - 這是一個忽略最終化和 Reference 類的簡化。 2 - 假設,Java 實現可以做到這一點,但這樣做的效能成本使其不切實際。