2.4.1 强根
强根这个概念相对容易理解,这里使用线程栈来演示这个概念。假设JVM执行一段Java程序,如下所示:
int a = 2; Object obj1 = new Object(); Object c = new Object(); { MyObject d = new MyObject(); //假设MyObject已经定义,且MyObject中有一个成员变量f指向Object d. f = c; // 地点一 } // 地点二
现在来模拟一下JVM执行过程中内存的使用情况,在代码的地点一,内存布局如图2-14所示。
图2-14 地点一内存布局
其中图2-14中栈空间的使用通常在编译时就可以确定,堆空间通常是在运行时才能确定。每一个局部变量a、b、c、d在栈中都有一个槽位(slot)与之对应,这样在程序中才能访问到它们指向的对象或者数值。
这里稍微提示一下,代码d.f = c并不是将栈中c的值赋值给d.f,而是将c指向的堆地址赋值给d.f。
当代码执行到地点二时,内存布局如图2-15所示。
图2-15 地点二内存布局
此时因为变量作用域,变量d在栈中将无法访问(实际上该槽位被其他的变量使用),变量d因为已经死亡,其对应堆中的内存(图中灰色空间)也应该可以被回收重用。
基于栈变量可以找到堆空间中所有活跃的对象。当然,如果变量d在GC执行时死亡,在活跃对象的遍历过程中并不能知道变量d是否存在过,也无法知道变量d指向的内存空间。整个GC结束后只能得到所有活跃对象所占用的内存空间,所以追踪的GC算法都是管理活跃对象(将活跃对象赋值到新的空间,即复制算法,或者从整个空间中剔除活跃对象后,采用列表的方式管理自由空间),从而达到内存重用的目的。
当然实现层面可能还有更多细节需要考虑,例如在栈中一个槽位存放的值到底是指向堆空间的变量(即指针)还是一个立即数(在上述代码中变量a就是一个立即数),对于立即数对象,GC并不需要遍历(因为没有在堆空间中分配内存)。但是GC执行时并不知道槽位到底是一个地址还是一个立即数,如果做不精确的GC,可以把立即数也“当作”指针,只要立即数在堆空间的访问范围内,也会把对应的内存空间进行标记;如果做精确的GC,则必须区分立即数和指针,所以通常需要额外的信息来保存指针信息(例如使用额外的位图来描述栈空间的哪些槽位是指针),在GC执行时借助额外的信息就可以进行精确的回收。
经研究发现,通常不精确的GC和精确的GC相比,性能会有15%~40%的差距。
从栈变量作为根的例子可以看出,如果缺少某一个根,则必然会遗漏一些活跃对象,从而导致GC会访问非法内存。所以必须找到所有的强根并且逐一遍历,才能保证垃圾回收的正确性。