5

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

我无法提供可以证实这一点的链接,但我已经阅读了很多关于它的文章。

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

4

2 回答 2

6

首先,我们需要确保我们与跟踪垃圾收集算法在其标记阶段所做的事情一致。

在任何给定时刻,跟踪 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;
}
  • 当垃圾收集开始时,我们得到一个根,即局部变量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;
}
  • 当垃圾回收开始时,局部变量Integer a是根,Foo this引用(所有非静态方法获得的隐式引用)也是根。局部变量Foo foofrommain也是一个根,因为main还没有超出范围。
  • 我们跟随根找到实例Integer和实例Foo(我们找到这些对象之一两次,但这对算法无关紧要),它们被标记为活动,然后我们尝试跟踪它们的非静态字段。假设 的实例Integer没有更多的类实例字段。的实例Foo为我们提供了一个Bar字段。
  • 我们按照该字段查找 的实例Bar,该实例被标记为活动的,然后我们尝试查找其非静态字段。我们发现它不再包含引用类型的字段,因此 GC 不再需要为该对象操心。
  • 由于在这一轮递归中我们无法找到新的活动对象,因此标记阶段可以结束。
于 2016-12-28T17:14:38.467 回答
3

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

于 2016-12-28T16:30:45.160 回答