垃圾回收算法——复制式回收

   标记-清扫回收地开销较低,但其可能受到内存碎片问题地困扰。在一个设计良好地系统中,垃圾回收通常只会占用整体执行时间地一小部分,赋值器地执行开销将决定整个程序的性能,因此应当设法降低赋值器的开销,特别是应当尽量提升它的分配速度。标记-整理回收器可以根除碎片问题,而且支持极为快速的“阶跃指针”分配,但它需要多次堆遍历过程,进而显著增加了回收时间。

半区复制算法:
该算法属于追踪式回收算法。回收器在复制过程中会进行堆整理,从而可以提升赋值器的分配速度,且回收过程只需对存活对象遍历一次。其最大的缺点在于堆的可用空间降低了一半。

1.半区复制回收

基本的复制式回收器会将堆划分为两个大小相等的半区,分别是来源空间和目标空间。当堆空间足够时,在目标空间中分配新对象的方法是根据对象大小简单地增加空闲指针,如果可用空间不足,则进行垃圾回收。回收器在将存活对象从来源空间复制到目标空间之前,必须先将两个半区的角色互换。在回收过程中,回收器简单地将存活对象从来源空间中迁出;在回收完成后,所有存活对象将紧密排布在目标空间地一端。在下一轮回收之前,回收器将简单地丢弃来源空间(以及其中的对象),但在实际应用中基于安全考虑,许多回收器在初始化下轮回收过程之前都会先将该区域清零。
与标记-整理回收不同,半区复制回收无须在对象头部引入额外空间。由于来源空间中的对象在复制完成后便不再使用,所以其每个槽都可以用于记录转发地址(至少在万物静止式回收中如此)。因此复制式回收甚至适用于不包含头部的对象。

2.遍历顺序与局部性

赋值器和回收器的局部性对程序性能有重要影响。如果回收器忽略对象之间的指针关系或者对象原本的分配顺序而任意将其移动到新位置,则很可能降低赋值器的局部性,进而降低整个程序的性能。我们需要在赋值器的局部性、回收器的局部性、回收频率之间做出一定的权衡。以标记-清扫和复制式回收的对比为例:在同等条件下,标记-清扫回收的可用堆大小是复制式回收的两倍,因此其回收次数会比后者少一半,于是我们可能会认为标记-清扫回收的整体性能更优。对于空间较小的堆,这一结论是正确的(他们使用的是分区适应分配以及非移动式回收器),但对于较大的堆,顺序分配提升了赋值器的局部性,提升了各个层次的缓存命中率,其所带来的性能收益明显高于标记-清扫回收的空间收益。对于更新频率较高的新分配对象而言,这一效果尤为明显。

3.需要考虑的问题

相对于非移动式回收(例如-标记-清扫回收),复制式回收具有两个显而易见的优点:

  • 分配速度快,同时可以根除内存碎片(假设不考虑字节对齐要求);
  • 简单的复制式回收器也比标记-清扫或标记-整理回收器更容易实现,但在相同的回收频率下,复制式回收所需要的虚拟内存是其他回收器的两倍。

3.1 分配
在经过整理的堆中进行内存分配的速度很快,其分配过程十分简单,通常只需要简单判断堆或者内存块的上限,然后返回空闲指针。如果使用块结构堆而非连续的堆,则判断偶尔会失败,此时需要使用一个新内存块。慢速路径(即判断失败情况下使用新内存块的代码路径)的出现频率取决于所分配的对象平均大小与内存块大小的比例。在多线程情况下,每个赋值器可以拥有一个独立的、无须与其他线程同步的本地分配缓冲区,因而其分配速度也会很快。该方案实现简单,且只需很少的元数据,相比只需,如果非移动式回收器要使用本地分配策略,每个线程可能需要一个独立的空间大小分级数据结构来实现分区适应分配。
阶跃指针分配的代码序列十分短小,更重要的是,这种线性分配方式具有较好的告诉缓存友好性。在半区复制策略下使用顺序分配算法来分配短寿命对象,意味着下一个分配的位置很可能是最近最少使用的,但现代处理器的预取能力可以解决在这种问题想可能出现的时间延迟。如果这一行为与操作系统的最近最少使用页淘汰策略产生冲突进而影响到换页性能,那么就需要考虑重新配置系统。对于复制式回收而言,欲使程序运行流畅,要么需要更多的物理内存,要么需要借助于其他回收策略。

3.2空间与局部性
半区复制最显而易见的缺点是需要维护第二个半区,有时也称为复制保留区。在内存大小一定,且忽略回收器所需数据结构的情况下,半区复制回收求的可用内存空间是整堆回收器的一半,这导致复制式回收器所需的回收次数比其他回收器更多。

3.3 移动对象
是否使用复制式回收器部分取决于移动对象的可行性及其开销。在某些环境下对象无法移动,一方面是由于缺乏精确类型信息,另一方面是由于对象被传递给了不允许引用发生变化的非托管代码。此外,在标记-清扫环境下的指针寻找问题通常比移动式回收器下的简单。对于非移动式回收器,找到指向某个存活对象的一个引用就已经足够,但移动式回收器却一定要找到并且更新指向被移动对象的全部引用。这一问题在并发移动式回收器中更加严重,因为指向某一对象的所有引用必须原子化地进行更新。
某些对象地复制开销很大,即使对象占用的空间很小,对其进行复制的开销仍可能会远大于标记操作的开销,但与此相比,指针追踪以及获取对象类型信息的开销和延迟往往更大。另外,一遍又一遍的复制不包含指针的大对象将会导致回收器性能的下降。一种解决方案是不复制大对象,转而将其交由非移动式回收器管理;另一种策略是使用虚拟的、非物理上的复制,要达到这一目的可以将对象保存在由回收器维护的链表中,或者将大对象分配在其专属的、可以二次映射的虚拟内存页上。

你可能感兴趣的:(JVM)