为什么非静态字段不能充当GC根?

gst*_*low 5 java garbage-collection member gc-roots

据我所知,静态字段(以及线程,局部变量和方法参数,JNI引用)充当GC根。

我无法提供可以确认这一点的链接,但是我已经阅读了很多文章。

为什么非静态字段不能充当GC根?

The*_*kis 5

首先,我们需要确定跟踪垃圾回收算法在标记阶段所执行的操作是否在同一页面上。

在任何给定的时刻,跟踪GC都具有许多已知的活动对象,从某种意义上说,它们可以被正在运行的程序按其当前状态访问。标记短语的主要步骤涉及跟踪那些对象的非静态字段以查找更多对象,并且现在也知道那些新对象仍然存在。递归地重复此步骤,直到遍历现有的活动对象都找不到新的活动对象为止。内存中所有未被证明是活动的对象都被视为已死。(然后,GC进入下一个阶段,即扫描阶段。对于此答案,我们不在乎该阶段。)

现在仅凭这一点还不足以执行算法。在一开始,该算法没有已知可以存活的对象,因此它无法开始跟随任何人的非静态字段。我们需要指定一组从一开始就被认为是活跃的对象。我们从理性上选择那些对象,因为它们不是来自算法的上一步,而是来自外部。具体来说,它们来自语言的语义。这些对象称为根。

在Java之类的语言中,有两组对象是明确的GC根。仍然可以在范围内的局部变量可访问的任何对象显然都是可以到达的(在其方法中,该方法仍未返回),因此它是活动的,因此是根。通过类的静态字段可访问的任何内容显然也可以访问(从任何地方),因此它是活的,因此是根。

但是,如果非静态字段也被视为根源,将会发生什么?

假设您实例化一个ArrayList<E>。在内部,该对象具有一个非静态字段,该字段指向Object[](表示列表存储的后备数组)。在某个时候,GC周期开始。在标记阶段,将Object[]标记为活动状态,因为它是由ArrayList<E>私有非静态字段指向的。在ArrayList<E>不被任何东西所指向,所以它不能被认为是活着。因此,在此循环中,ArrayList<E>在支撑Object[]幸存的同时销毁了。当然,在下一个循环中,它们Object[]也将消失,因为任何根都无法访问它。但是为什么要分两个周期进行呢?如果ArrayList<E>在第一个循环中该死角已经Object[]被使用,并且如果仅由一个死了的物体使用,那么是否也不Object[]应认为该死角在同一移动中是为了节省时间和空间?

这就是重点。如果要最大程度地提高效率(在跟踪GC的上下文中),则需要在单个GC中消除尽可能多的死对象。

为此,仅当证明封闭对象(包含该字段的对象)是活动对象时,非静态字段才应使该对象保持活动状态。相比之下,根是我们公理地称呼为活着的对象(没有证据),以启动算法的标记阶段。将后一类别限制在不破坏正在运行的程序的最低限度是我们的最大利益。

例如,假设您有以下代码:

class Foo {
    Bar bar = new Bar();

    public static void main(String[] args) {
        Foo foo = new Foo();
        System.gc();
    }

    public void test() {
        Integer a = 1;
        bar.counter++; //access to the non-static field
    }
}

class Bar {
    int counter = 0;
}
Run Code Online (Sandbox Code Playgroud)
  • 当垃圾回收开始时,我们得到一个根,即局部变量Foo foo。就是这样,那是我们唯一的根源。
  • 我们跟随根找到实例Foo,该实例被标记为活动状态,然后尝试查找其非静态字段。我们找到其中之一,Bar bar领域。
  • 我们遵循这些字段来查找Bar标记为活动的的实例,然后尝试查找其非静态字段。我们发现它不再包含引用类型的字段,因此GC不再需要为该对象烦恼。
  • 由于在这一轮递归中找不到新的活动对象,因此标记阶段可以结束。

或者:

class Foo {
    Bar bar = new Bar();

    public static void main(String[] args) {
        Foo foo = new Foo();
        foo.test();
    }

    public void test() {
        Integer a = 1;
        bar.counter++; //access to the non-static field
        System.gc();
    }
}

class Bar {
    int counter = 0;
}
Run Code Online (Sandbox Code Playgroud)
  • 当垃圾回收开始时,局部变量Integer a是一个根,Foo this引用(所有非静态方法都获得的隐式引用)也是一个根。Foo foofrom 的局部变量main也是一个根,因为main尚未超出范围。
  • 我们跟随根查找的实例Integer和的实例Foo(我们两次发现了这些对象之一,但这对算法无关紧要),它们被标记为活动的,然后尝试遵循其非静态字段。假设的实例Integer没有更多字段可用于对实例进行分类。的实例Foo给了我们一个Bar领域。
  • 我们遵循该字段来查找Bar标记为活动的的实例,然后尝试查找其非静态字段。我们发现它不再包含引用类型的字段,因此GC不再需要为该对象烦恼。
  • 由于在这一轮递归中找不到新的活动对象,因此标记阶段可以结束。


Mur*_*nik 3

非静态字段具有由包含它的实例持有的引用,因此它本身不能成为 GC 根。