昨日,有人在一个JVM群里问了一个问题,为什么跨代引用是gc root。这虽然是一个很简单的问题,但是其实涉及到了分代垃圾回收算法的核心理念。
gc root的基本解释
首先我们要理解一下GC root究竟是什么东西。
gc root堆是被我们垃圾回收所管理的内存空间。如图,存在两种引用,一种是堆外对象对堆内对象的引用,被标注为红色;另外一种是堆内对象之间的引用,被标注为灰色。通常我们说的gc root就可以被认为是红色的那种引用,比如说栈引用堆中对象。为什么我们不认为堆内对象之间的引用是gc root呢?因为我们的对象,最终是要被外部使用的,比如说被栈引用所访问。因此,如果一大堆的堆内对象之间互相引用,但是没有任何堆外部引用,那么这部分对象实际上也是不可达的。HotSpot就是如此的,所有的堆中的对象,最终都是被栈所使用的。因而,U和V就可以看做是不可达的对象了。
分代和跨代引用
解释了gc root的基本概念后,我们要来看看分代理论了。基本上,现代垃圾回收器都是分代垃圾回收器,它建立在两个分代理论之上:
- 弱分代假说(weak generational hypothesis):大多数对象在年轻的时候死亡;
- 强分代假说(strong generational hypothesis):越老的对象越难死亡;
这个分代假说引申出一种垃圾回收理念:将对象依据“年龄”分配到不同的区域,每次回收只回收其中的一个区域。这也就是分代回收的基础理念。因为很显然的,如果大部分对象都是朝生夕死的,那么将它们放在一起,每次回收都能够回收到很多的空间;剩下的不容易死亡的对象,放在一起,那么可以以一种极为低的频率来回收它们。这就兼顾了垃圾回收的时间开销和内存的空间利用率。
一般的垃圾回收算法至少会划分出两个年代,年轻代和老年代。但是单纯的分代理论在垃圾回收的时候存在一个巨大的缺陷:为了找到年轻代中的存活对象,却不得不遍历整个老年代,反过来也是一样的。
跨代引用引起老年代的遍历如果我们从年轻代开始遍历,那么可以断定N, S, P, Q都是存活对象。但是,V却不会被认为是存活对象,其占据的内存会被回收了。这就是一个惊天的大漏洞!因为U本身是老年代对象,而且有外部引用指向它,也就是说U是存活对象,而U指向了V,也就是说V也应该是存活对象才是!而这都是因为我们只遍历年轻代对象!
所以,为了解决这种跨代引用的问题,最笨的办法就是遍历老年代的对象,找出这些跨代引用来。这种方案存在极大的性能浪费。因为从两个分代假说里面,其实隐含了一个推论:跨代引用是极少的。也就是为了找出那么一点点跨代引用,我们却得遍历整个老年代!从上图来说,很显然的是,我们根本不必遍历R。
因此,为了避免这种遍历老年代的性能开销,通常的分代垃圾回收器会引入一种称为记忆集的技术。简单来说,记忆集就是用来记录跨代引用的表。
记忆集记录跨代引用如图,在拥有记忆集的情况下,我们就可以不用遍历老年代了,这是一个巨大的性能提升!
最终解释
现在,我们设想一下,要回收年轻代,首先我们要从引用年轻代对象的外部引用开始;其次,我们要从跨代引用开始。于是我们可以很自然的得出结果:跨代引用也是gc root。
整个模型可以抽象成:
gc root的最终解释
附录
在引入记忆集之后,其实会有一个很有意思的问题:即老年代对象即便已经事实上不可达了,但是因为记忆集的存在,会导致从该对象出发的跨代引用依旧会被当成gc root,直至该对象被回收引起记忆集中相关条目的擦除。
记忆集引出的问题如图,U已经不存在外部引用了,所以它事实上已经不可达了。但是在这个时刻,因为老年代没有发生GC,所以它依旧存活着。
- 如果我们采用遍历老年代的方法找出跨代引用,那么我们只能找到S->P这一条。于是U和V都会被当成是不可达对象,其内存空间就可以被回收掉了。
- 如果我们使用记忆集,那么因为U没有被GC掉,所以记忆集里面的条目U->V依旧存在,所以在年轻代回收的时候,V会被当成存活对象。
这个问题就是因为使用记忆集带来的“滞后性”,它提高了时间效率,但是却降低了空间利用率。不过无论如何,它依然确保了垃圾回收所遵循的原则:垃圾回收确保回收的对象必然是不可达对象,但是不确保所有的不可达对象都会被回收。
网友评论