了解垃圾收集器

原文期刊:Understanding Garbage Collectors.
垃圾收集(或GC)是一种为自动回收并重用内存的方法。其他语言中,需要手动分配和销毁对象,由于有了GC,Java程序员不需要收集和检查每个对象来决定其是否需要。相反,强大的GC程序在幕后工作,悄悄地丢弃不再使用的对象,并整理剩下的东西。这种脱脂提高程序的效率。

本文更新并摘录了“OpenJDK中的新垃圾收集器”一文,该文章最初发表于Java杂志2016年3月1日

什么是GC?

JVM将程序数据组织成对象。对象处于堆中,并包含字段属性。如下,表示一个简单的二叉树节点。

class TreeNode {
    public TreeNode left, right;
    public int data;
    TreeNode(TreeNode l,  TreeNode r, int d) {
        left = l; right = r; data = d;
    }
    public void setLeft(TreeNode l) { left = l;}
    public void setRight(TreeNode r) {right = r;}
}

现在,想象一下在这个类上执行的以下操作。

TreeNode left = new TreeNode(null, null, 13);
TreeNode right = new TreeNode(null, null, 19);
TreeNode root = new TreeNode(left, right, 17);

在这里,我创建了一个根为17、左子节点为13、右子节点为19的二叉树(参见图1).

在这里插入图片描述
图1.三节点树

假设然后替换右子节点,将子节点19保留为未连接的垃圾,得到图2

在这里插入图片描述
图2.替换一个子节点的同一棵树

可以想象,在构建和操作数据结构的过程中,堆将开始看起来像图3.

在这里插入图片描述
图3.包含许多未使用数据项的堆。

压缩数据意味着更改数据在内存中的地址。Java程序在特定地址找到一个对象。如果垃圾收集器移动对象,Java程序需要知道新的位置。最简单的方法是停止所有Java线程,压缩所有对象,更新所有数据地址以指向新地址,并继续Java程序。但是,这种方法会导致较长的暂停周期(称为GC暂停时间)。

应用程序没有运行自然是不好的。有两种常用的减少GC暂停时间的策略。GC文献称它们为并发算法(在程序运行时执行工作)和并行算法(停止Java线程,使用更多的GC线程来更快地完成工作)。JDK 8中的默认垃圾收集器(可以在命令行中手动指定-XX:+UseParallelGC)采用并行策略。它使用许多GC线程来获得可观的吞吐量。

并行垃圾收集器

并行垃圾收集器,根据对象存活了多少GC周期,将其分隔到两个区域-年青的和年长的。年轻对象最初在年轻区域中分配,压缩过程不会移动它们,直到它们经历了一定数量的回收周期。如果他们活得够久,他们就会晋升到老一辈。策略是,与其停下来收集整个堆(这将花费太长时间),不如只收集堆中可能包含短暂对象的部分。当然最终,也会有必要收集老年代。

文章只提到了两代,而其他文章提到了四代,具体是粒度的区别,还是维度的区别,有待后续学习。
为了只收集较年轻的对象,垃圾收集器需要知道老年代中有哪些指向了新生代。在旧对象指向了新对象的新位置时,需要将旧对象更新。JVM会维护一个总结数据结构(summarization data structure),也叫做卡片表(card table)。每当将引用写入到老年代的对象时,卡片表就会被标记,以便在下一个年轻代GC周期中,JVM可以扫描这张卡片,并寻找老年代对年轻对象的引用。在已知这些引用的情况下,并行垃圾收集器能够识别要筛选的对象和要更新的引用。它使用多个GC线程来更快地完成工作,同时暂停程序。

Garbage-First垃圾收集器

JDK的G1垃圾收集器同时使用并行算法和并发算法。它使用并发线程在Java程序运行时扫描活动对象。它使用并行线程快速复制对象,并保持较低的暂停时间。

G1将堆划分为许多区域。在程序运行期间,区域有可能是老年代,也可能是青年代。每一次GC暂停时都收集青年代,但是对于老年代,为用户指定的目标时间内完成,G1可以灵活的尽可能多或尽可能少得清理老区域。这种灵活性允许G1将旧对象GC的工作集中在堆中垃圾最多的区域。(这句话没看懂呢,为什么会有这种好处?)它还使G1能够根据用户指定的暂停时间对垃圾收集暂停时间进行调优。

如图所示图4,G1将自由地将对象压缩到新的区域。

在这里插入图片描述
图4.在G1运行之前和之后。第1区和第2区被压缩成第4区。新的物体可能被分配到第4区。第3区不受影响,因为对于太少的空间回收利用(30%),有用数据的复制工作太多(70%)。

G1知道每个区域有多少活数据,以及复制该活数据所需的大约时间。如果用户需要很小的暂停时间,G1可以选择只回收较少的几个区域。如果用户不担心暂停时间,或者已经声明了相当大的暂停时间目标,G1可能会选择回收更多的区域。

G1必须维护一个卡片表数据结构,这样才能只收集青年代。它还必须为其他老区域所引用的每一个老区域保存一份记录。这种数据结构称为变成记忆集(an into remembered set).

指定较小的暂停时间的缺点是G1可能无法跟上程序的分配速度,在这种情况下,它最终会放弃并退回到完全停止世界GC模式(是不是内存耗尽?)。这意味着扫描和复制工作都是在停止Java线程时完成的。注意,如果GC在进行局部垃圾回收时,不能满足暂停时间目标,那么会进行完整的GC,并且会超过分配的时间。

总之,总体来看,G1是一个很好垃圾回收器,它平衡吞吐量和暂停时间约束。

Shenandoah 垃圾收集器

shenandoah垃圾收集器是一个OpenJDK项目,它成为OpenJDK 12发行版的一部分,并被移植到JDK 8和11。它与G1一样,使用基于区域的堆布局,并使用相同的并发扫描线程来计算每个区域的活数据量。它处理压缩阶段的方式不同。

Shenandoah 并发地压缩数据。(目光敏锐的人会注意到,这意味着垃圾回收程序可能需要移动对象,同时应用程序会试图读取或写入对象;别担心–我一会儿就会谈到这一点。)因此,shenandoah不需要为了减少应用程序暂停时间而限制它收集的区域的数量。相反,它选择了所有最富有成果的区域–也就是那些有很少活数据的区域,也即是说,有很多垃圾的区域。只有一步会引起应用程序暂停,就是那些在扫描开始和结束时执行的簿记任务(bookkeeping tasks)。

shenandoah并发复制的关键困难是执行复制工作的GC线程和访问堆的Java线程需要就对象的地址进行适当的处理。此地址可能存储在多个地方,并且对地址的更新必须同时进行。就像计算机科学中最棘手的问题一样,解决办法是增加一个间接的层次。

对象为间接指针分配额外的空间。当Java线程访问对象时,它们首先读取间接指针,以查看对象是否已移动。当垃圾收集器移动对象时,它会更新间接指针以指向新位置。新对象会分配给一个指向自己的间接指针。只有在垃圾回收期间复制对象时,间接指针才会指向其他地方。

这个间接指针不是无消耗的。读取指针和查找对象当前位置会有在空间和时间上的消耗。这些消耗比你想象的要低。空间方面,shenandoah不需要堆外数据结构来支持局部垃圾回收,如卡片表和记忆集(card table and the into remembered sets)。时间方面,有各种策略来消除读取障碍。优化的JIT编译器可以识别到程序正在访问不可变的字段,例如数组大小。在这些情况下,读取对象的旧副本或新副本都是正确的,因此不需要间接读取。此外,如果Java程序从同一个对象读取多个字段,JIT可能会识别这一点,并删除转发指针的后续读取。(这句话没看明白呢。。。)

如果Java程序写入shenandoah正在复制的对象,则会发生争用条件。这是通过让Java线程与GC线程协作来解决的。如果Java线程即将写入已被标记需要复制的对象,Java线程将首先将对象复制到它自己的分配区域,并会检查它是否是第一个复制该对象的线程,然后执行写入。如果GC线程首先复制了对象,那么Java线程可以解除它的分配并使用GC副本。

在复制活动对象的过程中,shenandoah不需要暂停,从而在整体上提供了更短的暂停时间。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值