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

时间:2016-12-28 16:28:18

标签: java garbage-collection member gc-roots

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

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

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

2 个答案:

答案 0 :(得分:4)

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

在任何给定时刻,跟踪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。就是这样,那是我们唯一的根。
  • 我们按照root查找Foo的实例,该实例被标记为alive,然后我们尝试查找其非静态字段。我们找到其中一个,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 foo的本地变量main也是根,因为main尚未超出范围。
  • 我们按照root查找Integer的实例和Foo的实例(我们发现其中一个对象两次,但这对算法无关紧要),它们被标记为活着然后我们尝试跟随他们的非静态字段。假设Integer的实例没有更多字段来实现类实例。 Foo的实例为我们提供了一个Bar字段。
  • 我们按照该字段查找Bar的实例,该实例被标记为活着,然后我们尝试查找其非静态字段。我们发现它不再包含引用类型的字段,因此GC不再需要为该对象打扰。
  • 由于我们无法在这轮递归中找到新的活动对象,因此标记阶段可以结束。

答案 1 :(得分:2)

非静态字段的引用由包含它的实例保存,因此它本身不能是GC根目录。

相关问题