垃圾收集

C++方法 - 新增和删除

在像 C++这样的语言中,应用程序负责管理动态分配的内存使用的内存。当使用 new 运算符在 C++堆中创建对象时,需要相应地使用 delete 运算符来处理对象:

  • 如果程序忘记了一个对象而只是忘记它,那么关联的内存就会丢失给应用程序。这种情况的术语是内存泄漏,并且内存泄漏太多,应用程序可能会使用越来越多的内存,并最终崩溃。

  • 另一方面,如果某个应用程序尝试两次使用同一个对象,或者在删除该对象后使用该对象,则该应用程序可能会因内存损坏问题而崩溃

在复杂的 C++程序中,使用 new 和 delete 实现内存管理可能非常耗时。实际上,内存管理是错误的常见来源。

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 对象,并且对象都可以分别通过 n1,n2 和 n3 变量到达。语句 T4 将第二个 Node 对象的引用分配给第一个 next 对象的 next 字段。完成后,第二个 Node 可以通过两条路径到达:

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

在声明 T5 中,我们将 null 分配给 n2。这打破了 Node2 的第一个可达性链,但第二个仍然没有中断,所以 Node2 仍然可以到达。

在声明 T6 中,我们将 null 分配给 n3。这打破了 Node3 的唯一可达性链,这使得 Node3 无法到达。但是,Node1 和 Node2 仍可通过 n1 变量访问。

最后,当 test() 方法返回时,其局部变量 n1,n2 和 n3 超出范围,因此无法被任何东西访问。这打破了 Node1 和 Node2 的剩余可达性链,并且所有 Node 对象都不可达,并且有资格进行垃圾收集。

1 - 这是一个忽略最终化和 Reference 类的简化。 2 - 假设,Java 实现可以做到这一点,但这样做的性能成本使其不切实际。