深入学习 G1回收器和JVM:Full GC(7)

Evac失败

  • 失败处理主要是把对象放入Evac失败栈,同时会对栈对象进行处理,处理过程是串行的。处理过程发生在G1ParCopyColsure中,把对象加入到dirty card队列中处理。这么做的目的是,可以直接更新对象的RSet,不需要对已经复制的对象做额外回收之类的处理。
  • 如果Evac失败,则把对象的指针指向自己。所以整个JVM中如果发现指针指向自己则认为发生了复制失败。所以在处理Evac失败时,需要检查是否有指针指向自己。如果有,则需要删除指针,恢复对象头。

串行FGC

Evac失败后会进入FGC,在JDK10之前FGC都是串行回收。串行回收采用标记清除算法,主要分为:

  1. 标记活跃对象
  2. 计算新对象地址
  3. 把所有的引用都更新到新的地址上
  4. 移动对象完成压缩
  5. 后处理

    1. 尝试调整整个堆空间的大小。根据GC发生后已经使用的内存除以期望的占比得到期望空间大小。然后利用期望指和实际值的比较是否需要扩展或收缩堆空间。
    2. 遍历堆,重构RSet。因为所有的分区里面的对象位置都发生了变化,需要重构RSet,否者下一次GC就会丢失根集合,导致回收错误。
    3. 清除dirty card队列,将所有分区都认为是old分区。
    4. 最后记录各种信息,调整YGC的大小,重建Eden,用于下次回收。

并行FGC

FGC应该是努力要避免的,但是由于JVM不可控,在长时间运行的应用中FGC基本上不可避免。G1由于引入了分区,实现了一套并行的FGC。

  • 并行回收的每一步:

    1. 并行标记活跃对象:类似于并发标记,但是比并发标记简单,因为不涉及SATB处理。
    2. 计算对象的新地址:与串行FGC不同的是,串行处理中,每一个分区的有效对象都会移动到该分区的头部。而并行处理的时候,一个并发线程要处理多个分区,所以在计算对象的新地址的时可以把这一批分区里的对象进行压缩。分区的起始地址都是对象地址,因为除了大对象,对象不能跨分区存放。
    3. 更新引用对象地址:从根集合出发遍历活跃对象,然后把活跃对象和活跃对象中的引用都更新到新的位置。
    4. 移动对象完成压缩
    5. 后处理:恢复对象头,更新各种信息,如偏向锁恢复,更新新生代大小等。

参数调优

  • 参数MinHeapFreeRatio,这个值用于判断是否可以扩展堆空间,增大该值扩展概率变小,减小该值扩展几率变大。
  • 参数MaxHeapFreeRatio,这个值用于判断是否可以收缩堆空间,增大该值收缩概率变小,减小该值收缩概率变大。
  • 参数MarkSweepAlwaysCompactCount,默认值为4,这个值表示经过一定次数的GC之后,允许当前区域空间中一定比例的空间用来将死亡对象当作存活对象处理,这里姑且将这些对象称为弥留对象,把这片空间称为弥留。这个比例由MarkSweepDeadRatio控制,默认值为5,该参数的作用是加快FGC的处理速度。
阅读 205

推荐阅读