Fork me on GitHub

G1垃圾收集器

G1(Garbage-First)

G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

特点

压缩空闲空间不会延长GC的暂停时间。

内存模型

每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。
年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。
新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。
G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区(Region)的大小从1M到32M不等,但是都是2的冥次方。

  • 图例
    image

基本概念

  • 卡表(Card Table)
    基于卡表(Card Table)的设计,通常将堆空间划分为一系列2次幂大小的卡页(Card Page),HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每个标记项为1个字节,当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。
  • Region
    一个Region被分为多个Card Table,维护一个Rset。
  • RSet(Remember Set :记忆集合)
    由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么,需要跟踪从老年代到新生代的所有引用,从而避免每次YGC时扫描整个老年代,减少开销。JVM使用一个结构叫做Card Table来记录谁引用了我(point-in),这样在YGC阶段只需要扫描年轻代中各个Card Table中记录的老年代对象即可。
    在G1中又使用了一个新的结构Rset来记录我引用了谁(point-out),一个Region被分为多个Card Table,维护一个Rset,记录着引用到本region中的对象的其他region的Card。这样的好处是可以对region进行单独回收,这要求RSet不只是维护老年代到年轻代的引用,也要维护这老年代到老年代的引用,对于跨代引用的每次只要扫描这个region的RSet上的Card即可。
    • 图例
      image
  • CSet(Collection Set 回收集合)
    收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。
    CSet根据两种不同的回收类型分为两种不同CSet。
    1.CSet of Young Collection
    2.CSet of Mix Collection
    CSet of Young Collection 只专注回收 Young Region 跟 Survivor Region ,而CSet of Mix Collection 模式下的CSet 则会通过RSet计算Region中对象的活跃度,活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%),只有活跃度高于这个阈值的才会准入CSet,混合模式下CSet还可以通过XX:G1OldCSetRegionThresholdPercent(默认10%)设置,CSet跟整个堆的比例的数量上限。
  • SATB(Snapshot At the Begging)
    SATB,也可以称为对象快照技术,在GC之前对整个堆进行一次对象索引关系,形成位图,相当于堆的逻辑快照。在并并发回收过程中,通过增量的方式维护这个对象位图。
    • SATB解决了什么问题?
      主要是为了解决并发标记过程中,出现的漏标,误标等问题。
    • 什么是漏标,误标?
      在并发标记过程中,APP线程跟GC线程同时进行,GC线程扫描的时候发现一个对象位垃圾对象,不对他进行标记,而APP线程马上就会操作索引指向这个对象。那么在垃圾回收的时候这样漏标的对象被回收就会产生灾难性的后果。也有的情况就是,在GC标记完一个对象不需要回收之后,APP线程之后就会把所有指向这个对象的索引完全去除,那么这就是一个垃圾对象,然而在回收过程之中并没有回收,造成了浮动垃圾,这种情况就是误标。
    • 漏标、误标的解决方案?
      解决漏标误标,就必须了解两个名词。第一个名词:三色标记法,第二个名词:writer barrier(写栅栏)
      首先解释第一个名词:三色标记算法
      首先我们知道无论是 g1还是CMS垃圾标记算法都叫做根可达(root searching),首先搜索比如 线程栈上的对象、静态变量、常量池中的对象以及jni指针,这个部分往往发生是G1的初始标记阶段,会进行STW。然后就进入了并发标记阶段。首先我们定义:扫描过当前对象以及其子索引对象的为不可回收的对象位黑色对象,有黑色父对象索引指向的,并且未扫描其子索引的对象为灰色对象,需要回收的对象:为白色对象。
      image
      第二个名词:写栅栏:write barrier
      当上图中 B->C 改变成 A->C 的索引,垃圾回收器是如何感知的?就是通过writer barrier技术,其实就相当于一个钩子程序,但执行索引改变的时候,触发一下write barrier,然后write barrier根据相应的需求增加一条索引更改的日志。每个App现场都会有一个LTB(local thread buffer)当一个LTB缓冲区写满之后,就新起一个缓冲区,把原来的缓冲区写入全局缓冲中,又相应的垃圾回收线程去更新SATB 的对象快照图。
  • SATB + RSet 解决了什么问题?
    上面说了三色标记算法,为了解决漏标问题提出了一个writeBarrier的解决方案。但是还是有一种情况的漏标是writeBarrier解决不了的。就是在并发的情况下,当一个线程扫描对象A,对象A有索引:A->B,A->C,其中线程T1,扫描完B在扫描C的状态中,此时有个线程T2把索引B改动,改成A->D,把A设置为灰色,此时T1把C扫描完了,把A设置为黑色。这时我们就发现黑色对象A,下面就会有一个白色对象D未扫描。那么这样的漏标如何解决?
    SATB位图构建过程中,所有有索引改动的对象,如上面所说的D跟B,就放入一个队列中。当Remark阶段,扫描这个队列里面的所有对象,重新标记。但是重新标记,按照道理来说,我们又需要扫描整个堆,但是我们其实只想回收某一个Region,又去扫描整个堆效率上来说肯定是不行的。这个时候,我们就可以去扫描Region中的RSet,如果RSet 没有记录其他Region对这个对象的索引,自己内部也没有,那么这个对象就是一个可回收的垃圾对象。

YoungGC的工作流程

YGC 的工作流程很简单:APP线程跑,然后就进行青年代Region的回收,把需要回收的YoungRegion,放入YoungCSet中,在YGC阶段就进行对年轻代CSet中的Region进行回收。因为大部分都是垃圾,且用了复制回收算法,基本只需要较短时间的STW就能完全回收了。

MixGC的工作流程

当老年代垃圾达到一个阀值的时候,就会触发MixGC,就像第一幅图IPOH所标识的那样。阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动一次混合垃圾收集周期。在经过第一次YGC的同时,进行Init Mark,然后再后面几次YGC的整个过程中进行进行ConcurrentMark,并把需要收集的Region放入CSet当中。然后进行一次STW的时候,ReMark(重新标记,主要是扫描堆栈上新的对象的索引),并且进行Clean(主要的任务是直接清理没有用的大对象Region,也叫做HumongousRegion),然后就开始分步清理CSet中的Region,根据ReSet中计算出垃圾比率较高的Region开始清理。这一系列循环收集的过程称为混合收集周期(Mixed Collection Cycle)。

转移失败的担保机制 Full GC

我们看到第一幅回收流程图的过程,进行MixGC的同时也在并发的进行APP线程,产生了新的垃圾,如果这个时候发生了新产生的对象进入老年代Region,而堆空间不够的时候就会发生转移失败(Evacuation Failure),此时G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。但是有一个参数-XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,但是太大了一般会浪费空间,也没有太大的意义。

G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:

  • 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
  • 从老年代分区转移存活对象时,无法找到可用的空闲分区
  • 分配巨型对象时在老年代无法找到足够的连续分区

由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。

G1常用参数

  • -XX:+UseG1GC
  • -XX:MaxGCPauseMillis
    建议值,G1会尝试调整Young区的块数来达到这个值
  • -XX:+G1HeapRegionSize
    分区大小,建议逐渐增大该值,1 2 4 8 16 32。
    随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
    ZGC做了改进(动态区块大小)
  • G1NewSizePercent
    新生代最小比例,默认为5%
  • G1MaxNewSizePercent
    新生代最大比例,默认为60%
  • ConcGCThreads
    线程数量
  • InitiatingHeapOccupancyPercent
    启动G1的堆空间占用比例

总结一下

总的来说G1是一款非常优秀的垃圾回收器,现在也在企业应用的过程中越来越多的用到。其控制响应时间,跟简单的设置就能达到优秀的效率上面来说相对于于CMS等垃圾回收器来说无疑是巨大的进步。在我们发现使用CMS垃圾回收器时候了解不是很深刻,又或者对CMS调优方面已经在当前面临的情况没有好的办法了,尝试切换成G1垃圾回收器肯定会打来意想不到的收获。

但是在G1也有不少的缺点,因为有Concurrent Refine Thread 还有 RSet 等机制的存在一是对性能的影响,对内存的浪费都存在不少缺点。不过现在JDK15已经在2020.09.15面世,其中已经将ZGC(Zero GC作为主推的垃圾回收器),其管理内存在理论上已经达到了32T,而且MaxPauseTime 可以保证在10ms以内,无疑是巨大的进步。不过JDK15还不是LTS版本,所以只能说期待吧。有机会可以在以后的时间中与大家分享ZGC的内容。

参考

https://blog.csdn.net/qq_15965621/article/details/107899419
https://blog.csdn.net/chunqinling8330/article/details/101029748

posted @ 2020-05-02 12:14  晨度  阅读(2059)  评论(0编辑  收藏  举报