首先,我们需要确保我们对于跟踪垃圾收集算法在标记阶段的作用达成共识。
在任何给定时刻,跟踪 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
field.
- 我们按照字段查找实例
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
from main
也是一个根,因为main
还没有超出范围。
- 我们沿着根找到 的实例
Integer
和实例Foo
(我们两次找到其中一个对象,但这对算法来说并不重要),它们被标记为活动的,然后我们尝试跟踪它们的非静态字段。比如说下面的例子Integer
没有更多的类实例字段。的实例Foo
给我们一个Bar
field.
- 我们沿着字段查找实例
Bar
,它被标记为活动的,然后我们尝试找到它的非静态字段。我们发现它不再包含引用类型的字段,因此 GC 不再需要为该对象操心。
- 由于在这一轮递归中我们无法找到新的存活对象,因此标记阶段可以结束。