java虚拟机垃圾收集器

359 阅读8分钟

一、垃圾收集器概览

当前比较常用的垃圾收集器如下所示,

垃圾收集器 从表中可以看出,G1和ZGC分别作为技术最先进的商用及实验阶段的垃圾收集器,值得我们去了解。

二、G1收集器

G1之前的收集器目标为整个新生代、整个老年代以及整个Java堆的完全收集,G1的目标为基于局部收集,以达到停顿时间可控。

1、局部收集如何划分

G1收集器将连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据自身垃圾分布情况,动态扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理。Region如下图所示,H,代表Humongous,用于存储巨大对象,

G1内存布局 划分Region布局后,G1收集器根据用户设置的停顿时间,跟踪各个Region里面垃圾堆积的“价值”大小,即回收所获得的空间大小及所需时间的经验值,在不超出停顿时间的前提下,优先处理回收价值最大的那些Region。

2、如何做局部收集

挑选出需要回收的Region后,就可以对这些Region的垃圾对象进行清理,Region里面存在跨Region的引用,通过在每个Region都维护一个记忆集,这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页(一个卡页大概512字节)的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表(一个卡表包含多个卡页)的索引号。 即key为Region_i(除自身Region以外的任意一个)的地址,value为卡表索引号,只要卡表某个卡页存在对自身Region的引用,卡表值置为1,扫描跨代引用时对置1的卡表区域进行扫描。

记忆集 3、G1收集器的运作过程

G1收集分为四个步骤,如图,

G1收集示意图

  • 初始标记:标记GCRoots能直接关联到的对象,需要停顿线程,但耗时很短。
  • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理原始快照记录下的在并发时有引用变动的对象。
  • 最终标记:用户线程暂停,用于处理并发阶段结束后发生引用变动的原始快照记录。
  • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。

安全点:在GC线程与用户线程并发进行对象可达性分析过程中,对象引用关系可能会变化,安全点或者安全区域是指能够保证在一段代码片段之中,对象引用关系不会变化,在这个区域中任意地方进行垃圾收集是安全的。该区域一般选择方法调用、循环跳转、异常跳转等位置。

对比CMS的优势,由于仅回收一部分Region,停顿时间可以有用户控制,单个Region内看采用标记-整理算法,两个Region之间来看是复制算法,不会产生内存碎片;劣势在于记忆集更复杂,内存和性能开销大。

三、ZGC收集器

衡量垃圾收集器的三项最重要的指标是:内存占用、吞吐量和延迟,三者共同构成了一个“不可能三角”。随着计算机硬件的发展、性能的提升,延迟的重要性日益凸显。ZGC的目标在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障染色指针内存多重映射等技术来实现可并发标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。ZGC的Region布局具有动态性——动态创建和销毁,以及动态的区域容量大小,容量具有小、中、大型号,如下图,

ZGC堆内存布局 1、染色指针

从前,如果我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据,通常会在对象头中增加额外的存储字段,如对象的哈希码、分代年龄、锁记录等就是这样存储的,如Serial收集器将标记记录在对象头。考虑下面二种场景下访问对象:

  • 如果对象被移动过,即不能保证对象访问能够成功;

  • 不访问对象,但又希望得知该对象的某些信息。

G1收集器把标记记录在与对象相互独立的数据结构上(并发可达性分析的对象图,使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息),而ZGC的染色指针直接把标记信息记在引用对象的指针上,如图,

染色指针示意 Finalizable:是否只能通过finalize()方法才能被访问到,其他途径不行

Remapped:是否进入了重分配集

Marked1、Marked0:对象的三色标记状态,在并发的可达性分析算法中使用三色标记对象是否被收集器访问过

染色指针三大优势:

  • 一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。指针自愈见第3小节并发重分配部分;
  • 大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障,见第2小节;
  • 染色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

2、读屏障

ZGC在指针上更新Remapped标志位,在访问指针时加入读屏障。每个Region维护了一个转发表,记录从旧对象到新对象的转向关系。如果用户线程对象被GC移动过,而引用地址没有修改,这次访问会被读屏障截获,根据转发表,把指针更新为有效地址再访问。

3、ZGC收集器的运作过程

ZGC的运作过程大致可划分为以下四个大的阶段。全部四个阶段都是可以并发执行的,仅是两个阶段中间会存在短暂的停顿小阶段,这些小阶段, 譬如初始化GC Root直接关联对象的Mark Start,与之前G1的Initial Mark阶段并没有什么差异,不再解释。

ZGC运作过程

  • 并发标记:遍历对象图做可达性分析,它的初始标记和最终标记也会出现短暂的停顿,整个标记阶段只会更新染色指针中的Marked 0、Marked 1标志位。
  • 并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
  • 并发重分配:重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”能力。
  • 并发重映射:重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。

与G1对比:

  • G1 需要通过写屏障来维护记忆集,才能处理跨代指针。记忆集要占用大量的内存空间,写屏障也对正常程序运行造成额外负担。ZGC没有使用记忆集,不用写屏障,而是用全Region扫描代替记忆集的生成与维护。
  • ZGC染色指针占了4位,能管理的内存减小,能承受的对象分配速率不会太高。
  • ZGC吞吐量略超G1,停顿时间比G1低两个数量级。