JVM垃圾回收的根集合如何确定

simpleman7210 2013-11-13
我最近想在我的简单Java虚拟机实现里增加垃圾回收功能。关于垃圾回收我没有什么经验,但是垃圾回收的那些基本概念,这里或其它网站上有一大把的文章介绍,可以说我已经了解了。但是真要在Java虚拟机里实现垃圾收集,我首先就有一个问题:大部分的垃圾收集都采用从根集合出发,标记-清除-压缩之类的算法。那么根集是怎么确定的,并且,根集何时生成?我希望看到深入一点的文章,或者有人能说明下。按通常的说法,根集合主要由类的静态引用型变量和方法栈中的局部引用型变量构成,当要进行垃圾收集的时候,从根集合开始搜索和标记。那么,当要进行垃圾回收的时候,根集合是已经有了,还是先构造根集合?如果每次垃圾回收时先构造根集合,我感到难以想象,因为java方法的调用可能有许多层次,要搜索这么多方法栈帧的局部变量,构造出根集合,要花的代价可能很大啊。何况,这才是开始,接下来要从根集出发,搜索对象的实例变量,确定哪些对象是可达的。

关于根集合的一个主要来源 - 栈帧中的局部变量,我也有疑问。对此我要具体一点说明,举个例子,考虑垃圾回收如何工作。
class TestClass
{
private int _x;

public static void main(String[] args) {
	test();
}

public static void test() {
	int x = new TestClass()._x;
	TestClass c = new TestClass();
	Object o = c.getObject();
}

Object getObject() {
	return getObject1();
}

Object getObject1() {
	return getObject2();
}

Object getObject2() {
	return new Object();
}
}


这个例子中,main()调用test()。用javap看test()编译后的字节码如下:
public static void test();
  Code:
   0:   new     #3; //class TestClass
   3:   dup
   4:   invokespecial   #4; //Method "<init>":()V
   7:   getfield        #5; //Field _x:I
   10:  istore_0
   11:  new     #3; //class TestClass
   14:  dup
   15:  invokespecial   #4; //Method "<init>":()V
   18:  astore_1
   19:  aload_1
   20:  invokevirtual   #6; //Method getObject:()Ljava/lang/Object;
   23:  astore_2
   24:  return


test方法里有3个局部变量。test源代码的第一行,new一个临时对象,此对象引用位于操作数栈上(可以安全被回收)。第二行,new一个对象,然后把它保存到局部变量c中。我们知道,new指令总是把对象引用放到操作数栈中。但是第二行,在把这个对象引用从操作数栈存到局部变量c之前,垃圾回收应该不允许工作,不然就把这个刚刚建立的TestClass对象回收掉了。再看test源码第三行,调用一个方法getObject(),而这个方法又去调用别的,调用了好几层。并且在这个过程中,最终返回的对象引用,一直只存在于操作数栈中,直到getObject返回之后,才从操作数栈中存到局部变量o中。显然,在getObject2中,产生的这个对象(其引用存在于操作数栈中),也不允许被垃圾回收掉。

我举这个例子想说明,对象引用可能存在局部变量中,也可能存在操作数栈中。何时放入根集合中?似乎这里面是有规则的。这规则是什么呢
RednaxelaFX 2013-11-13
常见的做法是,在JVM要开始GC的时候才构建这次GC的根集合。
在许多JVM实现中,根集合的主要构成是:

1、JVM内部的全局数据中指向Java对象的指针。
这种主要是像必须要加载的类(java.lang.Object、java.lang.String、所有包装类型、java.lang.Thread等等);
2、线程的栈上所有指向Java对象的指针。参数与局部变量(局部变量区)和临时变量(操作数栈)都包括在内。JNI的local handle也算在这边;
3、还有JNI persistent handle,这个算特殊的全局数据,特别拎出来讲;
4、如果是分代式GC,当前要做的GC是较年轻的分代,那么需要把较年老的分代里指向当前分代的指针也算作根集合的一部分。

上面情况中,
1最好办,每次遍历过去的东西都在同样位置;
2可以参考这篇 http://rednaxelafx.iteye.com/blog/1044951。每次GC都必须扫描所有Java线程的栈来枚举出它所构成的根集合。开销固然大但还是得做;
3其实跟其它全局数据的处理方式差不多,它的“头”通常还是在已知地址上的;
4请读一下分代式GC相关的文章,常见做法是使用write barrier来维护remember set/card table来记录跨代指针。
simpleman7210 2013-11-14
我看了第2点的你的这篇文章,但还是没有弄明白OopMap(或者GC Map)。也许过一段时间我再回到这个话题。主要关注的还是第2点。我差不多赞同“GC开始的时候构造根集合,开销虽然大还是得做”。我曾想让虚拟机启动的时候构造根集合,然后每次进入方法,以及从方法返回就更新根集合。这个想法没有经过检验。不过我意识到即使这样能行,它也带来额外的开销,就是随着方法栈的伸展和收缩,需要不停地更新根集合,而垃圾收集通常只在内存不够的时候才启动,这样平时对根集合的更新就做了许多无用功。
Global site tag (gtag.js) - Google Analytics