JVM垃圾回收器详解:串行回收,新生代内存管理,引用集管理

引用集管理

在分代算法中,位于不同代之间的对象可能存在相互引用。

在应用初始运行时,对象都位于新生代,对象之间的引用关系都在新生代中。但是当对象晋升到老生代后,此时就存在新生代中的对象引用老生代的对象,同时老生代对象引用新生代对象的情况。在对新生代进行垃圾回收时,第一步需要做的就是识别新生代中的活跃对象。由于代际之间存在相互引用的情况,按照常规的思路,需要对整个内存空间进行标记才能准确地识别内存中的活跃对象。在对整个空间标记完成之后,再对新生代的空间进行回收。

为了回收部分空间,对整个空间进行标记存在大量的浪费。为了解决这个问题,分代设计中引入了一个概念,称为“引用集”。引用集主要记录从老生代到新生代的对象引用关系,用于加速Minor GC时对象的标记。在对新生代进行标记时,把引用集作为新生代的根,从引用集找到的对象都认为是活跃的,这样就不用标记整个内存空间。

注意

在目前的设计和实现中,所有的垃圾回收器都只有老生代到新生代的引用,原因在于回收老生代时要么同时回收新生代,要么要求先回收新生代再回收老生代。对于这两种设计来说,都不需要额外记录新生代到老生代的引用。在同时回收新生代和老生代时,需要对整个内存空间进行标记,所以无须进行额外的记录;在回收老生代时首先进行一次新生代的回收,可以直接把新生代回收之后的对象作为老生代的根,所以也无须额外的记录。不记录新生代到老生代的引用的主要原因是:新生代发生垃圾回收的频次较高,对象的位置变化频繁,这样的变化会导致引用集的设计非常复杂。

当然,使用引用集的方法可能会导致一部分浮动垃圾无法回收。例如老生代的对象实际上已经死亡,若对象仍然引用到新生代,引用集仍然会把这些死亡对象作为新生代的根,而把死亡对象作为根会导致浮动垃圾。

注意,老生代中死亡的对象只有在老生代发生垃圾回收之后才能被识别出来,只有识别之后才可能更新引用集。

应该设计什么样的数据结构来存储引用关系及何时记录引用关系?引用关系的记录大体可以分为以下两种方式。

(1)在引用对象处记录引用关系

因为引用对象在一个时刻只能指向一个被引用对象,所以这个引用关系只需要记录一次。老生代中的对象只有在发生了老生代回收后位置才可能发生变化,在新生代回收时老生代中的对象位置不会变化,所以可以通过老生代中的对象关联一个数据结构来记录引用关系。例如在实现时,可以直接分配一个数组,数组的下标是老生代中对象的地址,数组元素对应的值为引用新生代对象的地址,如图3-23所示。

图3-23 数组记录引用关系

这样的实现有一个小小的缺点:数组比较大(记录完整的对象地址),导致空间消耗大。另外,在实际运行过程中,数组的大多数元素都未使用,是一个非常稀疏的数组。所以一个优化方法是采用压缩方式存储数组。通常把一段内存空间视为一个管理单元(简称为卡块),如果管理单元中有任何一个对象存在指向新生代的引用,那么就认为该单元中所有的对象都有可能存在指向新生代的引用,在处理引用时,再把该单元中所有的对象一一取出,并判断是否存在指向新生代的引用,如果存在则进行标记,如果不存在则直接跳过对象。这是一种典型的时间换空间的做法。这种技术被称为卡表。使用卡表时需要考虑两个问题:

1)卡表的大小是一个值得关注的问题。

2)存储不能以对象为单位进行管理,因为对象的大小都不相同。所以使用两个压缩表,一个记录引用,一个记录对象的起始位置。

使用卡表存储引用关系的示意图如图3-24所示。

图3-24 使用卡表存储引用关系

使用卡表还需要解决另外一个问题——如何访问对象?访问对象时总是需要知道对象的起始地址才能读取对象,然而卡表和对象的起始地址没有任何关系,所以需要一个额外的数据结构记录每个卡块中第一个对象的位置(这个值是第一个对象起始地址和卡块起始地址的偏移量),这个数据结构在JVM中被称为BOT(Block-Offset-Table),这样就能正确地访问卡表的对象了。

但是有一个特殊的场景,需要对BOT信息进行额外处理,就是大对象的处理。一个大对象会占用多个连续的卡块,要找到超大对象的起始地址,可以在BOT中记录一个负值表示对象起始地址在前一个卡块中,这样通过BOT的配合总能找到对象的起始地址。

这个方案针对超大对象可能不够优化,需要连续访问多个BOT表,不断地往前追溯。一个可能的优化是在BOT表中直接记录一个目标位置的负值,然后就可以通过该负值直接跳到目标位置的卡块中,从而减少了追捕回溯的性能。大对象回溯示意图如图3-25所示,其中用x表示当前修改位置和对象头所在位置的偏移距离。

图3-25 大对象BOT回溯示意图

实现中还需要考虑一些细节,例如对象超级大,图3-25中的BOT存储的值-x超过了BOT一个元素的表示范围,此时需要设计一个合理的编码方式记录-x。JVM也是类似的实现,具体的编码规则不再展开介绍。

(2)在被引用对象处记录引用关系

因为多个对象可以同时指向同一个引用对象,所以在这种方法中需要记录多个引用者。回收时只需要简单处理自己对象存储的引用关系,如图3-26所示。

图3-26 在被引用者处记录引用者信息

在串行回收中通过写屏障技术来记录引用关系。写屏障指的是在堆空间中写对象时,额外插入一段代码。除了写屏障以外,还有读屏障、比较屏障等概念。由于卡表记录的是代际引用,代际引用关系变化发生在Minor GC或者Mutator执行过程中,如果对象发生了晋升、转移或者引用关系修改,也就意味着发生了对象写操作,就可以通过写屏障技术将引用关系记录在卡表中。

串行回收中通过卡表管理引用关系,主要原因是:在堆设计时地址连续、边界固定,非常适用于使用卡表快速判断是否需要写屏障。由于记录引用关系需要屏障技术,这意味着需要存储成本以及执行成本,因此很有必要确定哪些情况需要使用卡表来记录引用关系。对象修改前后引用关系是否需要记录的情况如表3-2所示。

表3-2 对象修改前后引用关系是否需要记录的情况

在JVM的实现中,对于卡表的处理涉及读、标记、写。处理方式是:先找到卡表中存在引用标记的卡块,对该卡块进行清除,然后对卡块关联的对象进行遍历,判断对象是否存在指向新生代的引用,如果存在,则进行标记、转移,如果不存在则跳过。当对象转移后,把原来的引用地址更新为新的地址,在更新成功后,再次判断是否需要记录引用关系,如果需要则再次对引用集进行更新。当对象晋升到老生代时,晋升的对象会再次被扫描,相当于认为这些对象存在代际之间的引用关系。

本文给大家讲解的内容是JVM垃圾回收器详解:串行回收,新生代内存管理,引用集管理

  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值