垃圾回收算法手册读书笔记(五) 分代垃圾回收

分代垃圾回收

垃圾回收的主要目的是找到已经死亡的对象并且回收其所占用的内存。在堆内存分区中有提到,对于长寿对象,反复的标记,移动这些对象将影响垃圾回收的效益。弱分代假说告诉我们,大部分对象在其年轻时死亡,利用这一特性,开发人员根据对象年龄进行分代(分区),将年轻对象放入年轻代,将长寿对象放入老年代。回收器优先回收年轻代,并将其中年龄足够长的对象提升到更老的一代。

分代垃圾回收希望通过较多的次级回收(minor collection)与较少的主回收(major collection),来达到降低停顿时间的期望

示例

分代垃圾回收

上图是一个分代垃圾回收的简单示例,它使用两个分代,每个新创建的对象被分配在年轻代,回收器优先回收年轻代对象,并负责将年轻代中寿命够长的对象提升到老年代。其中有几个问题需要考虑:

  • 对象年龄如何测量:分代垃圾回收器的其中一个重要任务便是把寿命足够长的对象提升到老年代,而达到这一目的首先需要一种测量对象寿命的方法。
  • 分代间指针:在介绍堆内存的划分时有提到,在对某一个分区进行单独的垃圾回收时,必须找出其他分区中指向本分区中对象的指针,并追踪持有这些指针的对象。如图中的对象S,U,在年轻代垃圾回收时,S,U也必须作为根对象进行追踪,这些持有分代间指针的对象通常会通过一种数据结构保存起来方便查找,这种数据结构称为记忆集
  • 分代间指针对年轻代对象的”庇护”:图中对象U,其自身已经不可达,却持有指向对象V的一个指针,导致在年轻代回收时无法将对象V回收,这一现象称为庇护效应,这一效应将造成浮动垃圾(已死亡但由于各种原因并未在垃圾回收中被回收的对象)的增加,并且很有可能这些浮动垃圾会被提升至老年代,从而降低垃圾回收的效益。

对象寿命测量

一般来说测量对象寿命有2种方式

  • 基于对象所经历的时间
  • 基于堆所分配的字节数

基于堆字节数的分配能够很好的衡量内存分配的压力,这一压力很大程度上与垃圾回收的频率相关。然而在多线程情况下对于记录堆分配的字节数有着困难,因此一般使用对象所经历的垃圾回收次数作为其寿命,这是一种字节分配测量方法的近似替代。

分代假说

  • 弱分代假说:对象约年轻约容易死亡
  • 强分代假说:对象约老约不容易死亡

年轻代回收与过早提升

分代垃圾回收的一个主要设计目标就是降低停顿时间的期望,由于年轻代回收远多于老年代的回收(理想状态下),因此停顿时间的期望很大程度上取决于年轻代回收的停顿时间,而这又取决于回收过后年轻代的存活对象数量。

过早提升指的是过早的把年轻代对象提升至老年代,一般来说年轻代的空间太小将会导致过早提升,如果年轻代空间太小,将会带来以下不良影响:

  • 导致空间很快耗尽,进而导致年轻代垃圾回收间隔太短。
  • 回收间隔太短,以至于没有足够多的对象在这期间死亡,大量的存活对象加大了垃圾回收的开销,同时占用年轻代空间导致情况进一步恶化。
  • 频繁的年轻代回收将会导致年轻代对象快速的提升到老年代,导致老年代被快速的填充,进而提高老年代甚至全堆的垃圾回收频率。
  • 这些被快速提升至老年代的年轻对象造成的”庇护”效应导致更多的年轻代对象存活。

年龄记录

集体提升

当回收器对某一分代进行回收时,直接将其中的存活对象提升到下一代。策略的优劣在于:

  • 无需记录对象的年龄
  • 无需额外的复制保留区
  • 提升率较高,如果一个对象在回收的前一刻被创建,那么其会立马被提升到老年代,造成过早提升。

衰老半区

年轻分代划分来源空间与目标空间,要求对象在提升到老年代之前已在来源空间与目标空间之间复制多次。衰老半区策略又可以分为:

  • 记录空间年龄的衰老半区:如果回收器在来源空间与目标空间之间复制达到一定次数,将目标空间对象集体提升至老年代
  • 记录对象年龄的衰老半区:记录每个对象在两个半区间的复制次数,并将到达一定次数的对象提升至老年代

存活对象空间

将年轻代划分为一个较大的诞生空间与两个较小的存活对象空间,两个存活对象空间又分为存活对象来源空间(survivor fromspace)与存活对象目标空间(survivor tospace),每次回收器对年轻代进行回收,将存活对象从诞生空间和存活对象来源空间,复制到存活对象目标空间,之后将来源空间与目标空间进行交换(即原目标空间成为来源空间,原来源空间成为目标空间)。

与衰老半区不同的是诞生空间与存活对象空间的比例可以配置。在HotSpot Jvm中这一默认值为8:1,这样有利于内存空间的利用,不足之处在于当存活的对象大小超过存活对象目标空间大小时,只能将多余的对象直接分配进老年代。

分代间指针

当对某一分代进行单独垃圾回收时,回收器的根不仅包含通常意义上的根(寄存器,栈,全局变量等),还应当包括其他分代指向该分代的对象(如图9.1中的S,U对象)。分代间的指针在以下三种情况产生

  • 对象创建时
  • 赋值器更新时(比如将老年代对象引用写入年轻代对象的域中)
  • 对象移动到其他分代时

记忆集

我们需要一种数据结构对这些分代间指针进行记录,这种数据结构称为记忆集。一般来说。每个会单独回收的分代都需要配备一个记忆集来记录指向该分代内对象的分代间指针。

记忆集的维护需要额外的开销,除开记忆集数据结构的空间占用,还需要赋值器写屏障来拦截记录分代间指针的产生。

记忆集的记录精度并不是越高越好,较高的精度意味着更高的维护记忆集开销。例如记忆集可以记录每一个分代间指针的具体地址,也可以记录分代间指针所在的一个地址段。相反的,精度越低,回收器在进行回收时的开销就越高,如果记忆集仅记录某一地址段是否有指针指向该分代,则回收器需要将该地址段中的所有对象视为根进行追踪

指针方向

如果我们能够对各分代回收顺序进行一定的限制,如果能够确保对老年代回收时年轻代也一定回收,则不需要记录年轻代到老年代的指针

需要考虑的问题

  • 分代垃圾回收依赖于弱分代假说,一旦应用的绝大多数对象不在年轻时死亡,那么分代回收的策略可能不再适用
  • 分代垃圾回收所能降低的是停顿时间的期望,而非最大停顿时间。回收器最终都会进行整个堆的回收,因此分代垃圾回收并不适用于对于停顿时间敏感的应用。