新生代回收(YOUNG GC,YGC)

在内存分配的时候,剩余空间不能满足要分配的对象时,就会优先触发YGC。
G1每次回收的内存和分区个数可能并不相同,但是每一次YGC都收集所有的新生代分区,所以每次YGC之后都会调整新生代分区的数目。

YGC算法概述

YGC的执行顺序

  1. 进行收集之前需要STW
  2. 选择要收集的CSet,对于YGC来说,整个新生代分区都是CSet
  3. 进入并行处理任务

    • 根扫描并处理:处理过程会把根直接引用的对象复制到新的Survivor区,然后把被引用的field入栈等待后续复制处理。
    • 处理老年代分区到新生代分区的引用:先更新RSet,然后从RSet出发,把RSet所在卡表对应的分区内存块中所有的对象都认为是根,把这些根引用的对象复制到新的Survivor区,然后把被引用对象的field入栈等待后续的复制处理。
    • JIT代码扫描:根据栈中的对象,深度递归遍历复制对象。
  4. 其他任务处理(大部分是串行处理)

    • JIT代码位置更新::更新相关指针所指向的位置。
    • 引用处理:把引用中使用的存活对象复制到新的分区。
    • 字符串去重优化回收:G1的新功能,优化字符串的使用效率。
    • 清除卡表:把全局卡表中已经处理过的分区对应的卡表清空。
    • JIT代码回收:代码已经可以回收,实际上是删除相关引用。
    • Evac失败处理:如果Evac失败处理,则进行处理,主要是恢复对象头。
    • 引用再处理:把引用中还活着的对象放入引用队列。
    • redirty:主要就是重构RSet
    • 释放CSet:启动释放内存,把这些分区放入自由列表(Free List),如果对象分配时需要新的分区,可以从自由列表中获取。
    • 尝试回收大对象:判断这些大对象分区是否有RSet引用,只需要判断大对象所在第一个分区,如果没有引用则说明整个大对象都死亡了。
    • 尝试拓展内存:根据GCTImeRatio和G1ExpandByPercentOfAvailable来判读是否可拓展,如果可以,拓展多大的内存。
    • 调整新生代分区的数目:调整Refinement Zone的阈值等。主要更具GC的执行时间和目标停顿时间预测下次可能发生垃圾回收时能接受的最大的分区数。
整体流程

img

YGC代码分析

并行任务

并行任务是通过FlexibleWorkGang来执行G1ParTask
  1. 根扫描并处理,针对所有的根,对可达对象做:

    1. 如果对象没设置过标记信息,把对象从Eden复制到Survivor,然后针对对象每个field

      1. 如果field所引用的分区在CSet,则把对象的地址加入到G1ParScanTreadStates(PSS)的队列中待扫描。
      2. 如果字段不在CSet,则更新所在堆分区的RSet
    2. 更新根对象到对象新的位置:

      1. 当发现对象需要被复制,先复制对象到新的位置
      2. 复制之后把老位置的引用对象头标记为11,然后把老对象头里的指针指向新位置的引用。
  2. 处理老年代分区到新生代分区的引用

    1. 处理Dirty Card,更新RSet,更新老年代分区到新生代分区的引用。
    2. 扫描RSet,把引用者作为根,从根出发,堆可达对象进行根扫描并处理。
  3. 复制。在PSS队列中的对象都是活跃对象,每一个对象都要复制到Survivor区,然后针对该对象的每一个字段:

    1. 如果字段所引用的分区在CSet,则把对象的地址加到PSS的队列中待扫描;
    2. 循环1 直到没有对象。

根处理

JVM的根在这里也称为强根,指的是JVM的堆外空间引用到堆空间的对象,有栈或者全局变量等。整个根分为两大类:
  • java根:主要指类加载器和线程栈。

    • 类加载器主要是遍历这个类加载器中所有存活的Klass(指jvm对java对象的元数据描述)并复制到Survivor或者晋升到老年代。
    • 线程栈既会处理普通的java线程栈分配的局部变量,也会处理本地方法栈访问的堆对象。
    • jvm根:通常指全局变量

RSet处理

RSet处理的人口在G1RootProcessor::scan_remembered_sets
  • 更新RSet就是把引用关系存储到RSet对应的PRT中。
  • 扫描RSet则是根据RSet的存储信息扫描找到对应的引用者(即根)
  • 因为RSet内部有3种不同粒度的存储类型,所以根的大小也会不同。
更新RSet

Refine线程处理绿,黄,红区。白区则由YGC来处理,处理方式与Refine线程一样。

扫描RSet:(每个GC线程都只会针对部分的分区处理,所以它们之间能并行运行)

扫描RSet会处理CSet中所有待回收的分区。先找到RSet中的老年代分区对象,这些对象指向CSet中的对象。然后对这些老年代对象处理,把老年代对象field指向的对象的地址放入队列中待后续处理。

引用者分区处理

找到卡表所在的区域,RSet中存储的是对象起始地址所对应的卡表地址,所以一定能找到对象。

img

RSet里面的每一个PRT存储的就是对应卡表的位置(即指针)。

在图中我们假设对象1、2、3分配连续图中第2个卡表所指向的内存。由于卡表是按照512字节对齐,所以对象1、2、3的卡表指针是相同的。

当对象1、2、3之一引用到新生代的对象时,在新生代里面的PRT都只能找到图中第2个卡表的起始位置。而这个卡表指针不能明确地说明是对象1、2或者3,所以当通过RSet找引用者的时候,这个指针只能理解为对象1、2、3都可能引用到新生代了。

针对这个情况,要找到准确的引用者,必须有以下两步
  1. 先找到对象1的起始位置。G1通过使用G1BlockOffsetTable来记标记对象所在块的起始位置。
  2. 遍历从第一个对象到最后一个对象为止,查找对象1,2,3所有的field是否都有到待回收分区的引用,如果有,说明该field是一个有效的引用,把该field放入待处理队列用于后续的遍历和复制。
复制

复制Evac处理:实际上就是将在java根和RSet根找到的子对象全部复制到新的分区中。如果一切顺利,在CSet中所有活跃的对象都将被复制到新的分区中,并且在复制的过程中,引用关系也随之处理。如果发生了失败,处理流程也基本类似。

GC如何进行并行处理

  • 对于java根处理来说,根对象有多个,所以分配一个数组来存储各个并行任务的状态,在使用的时候多个线程通过CAS来获取数组中的元素来保证并行执行任务。
  • 对于RSet根来说,处理的时候是根据分区来进行处理。即使老年代对象中引用了多个CSet中不同的分区,也没问题,因为这时候仅仅是标记出对象,即使一个对象被处理多次也没问题。
  • 在Evac中,因为每次处理对象的时候,需要对对象进行复制,这个时候是需要多个线程使用CAS来保证串行,先把对象标记为待回收,之后才能复制。即只能由一个线程复制成功,其他线程都会重用这个新对象的复制。

    • Evac由于设计到对象的复制,这个非常耗时,所以在这个阶段还提供了任务窃取功能。在并发执行的过程中,GC线程优先处理本地的队列。当本地的队列没有任务的时候,窃取其他队列的任务,帮助别的队列。因为Evac保证了并行执行时的冲突问题,所以从别的对象队列中取几个待处理对象直接处理即可。

其他处理

其他处理大部分是串行处理,并且大多是在并行工作结束后开始,大多是因为处理过程需要同步等待,需要独占访问临界区。(除了入Redirty,字符串去重和引用)

参数调优

  • 参数ParallelGCThreads,默认值为0,表示的是并行执行GC的线程个数。G1可以根据CPU的个数自行推断线程数;GC是CPU密集型的任务,通常来说线程个数不应该超过CPU核数一般不用设置该值
  • 在对新生代收集的过程中,如果对象在YGC发生了一定次数之后还存活,这意味着对象有很大的概率存活更长的时间,所以通常会把它晋升到老生代。而这个次数可以通过参数MaxTenuringThreshold控制,默认值是15,即发生15次YGC后,对象仍然存活,存活的对象会晋升到老生代。这个值最大只能是15。减小该值可以会把对象更早地提升到老生代。
  • 参数G1RsetScanBlockSize,默认值为64,指扫描Rset时一次处理的量,其目的是为了加速处理速度;如果计算能力较强,可以增大该值
  • 参数SurvivorRatio,默认值为8,指Eden和一个Survivor分区之间的比例;减小该值,将导致Survivor分区大小变大,G1中并不会因为增大该值直接导致Eden变小,Eden是根据GC的时间来预测的
  • 参数TargetSurvivorRatio默认值为50,表示期望Survivor的大小。增大该值,则用于下一次Survivor的空间变大,晋升到Old分区的概率会减少。
  • 参数ParGCArrayScanChunk,默认值为50,表示当一个对象数组的长度超过这个阈值之后,不会一次性遍历它,而是分多次处理,每次的长度都是这个阈值,只有最后一次处理的长度在ParGCArrayScanChunk和2×ParGCArrayScanChunk之间。减小该值会减少栈溢出的情况,增大该值效率会略有提升。G1中的处理和其他的收集器略有不同,其他的收集器中当使用对象压缩指针,并且发生Evac失败时可能导致信息丢失,所以如果你在使用其他的收集器,当发生这种问题时,可以XX:UseCompressedOops或者把ParGCArrayScanChunk设置成最大的对象数组长度,即永远都不要对对象数组分多次处理。
  • 参数ResizePLAB,默认值为true,表示在垃圾回收结束后会根据内存的使用情况来调整PLAB的大小,但是目前G1中的GC线程在不同的阶段如Evac,引用处理等都会涉及内存分配,所以在PLAB的调整上是根据整体内存的使用情况进行的,这个成本比较高。因此在一些基准测试中发现禁止该选项可能有更好的效果,但这并不一定也适用于你的应用,关于PLAB效率和性能有一个bug,如果使用该选项也可以进行调整并测试。关于PLAB在JDK9等后面的版本中会引入相关参数。
  • 参数YoungPLABSize默认值为4096,是新生代PLAB缓存大小。在32位JVM中PLAB为16KB,在64位JVM中为32KB,表示对象从Eden复制到Survivor时,每次请求16KB作为分配缓存,提高分配效率。增大该值可以提高分配的效率,但是可能增加内存碎片,同时可能使得S分区很快耗尽;实际调优中可以尝试先减小该值。
  • 参数OldPLABSize,默认值为1024,指老生代PLAB缓存大小。在32位JVM中PLAB为4KB,64位JVM中为8KB,表示对象从Eden复制到Old时,每次请求4KB作为分配缓存,提高分配效率。增大该值可以提高分配的效率,但是可能增加内存碎片;通常来说Old分区空间更大,实际调优中可以尝试先增大该值。
  • 参数ParallelGCBufferWastePct,默认值为10,表示对象从Eden到Survivor或者Old区的时候,如果剩余空间小于这个比例,且不能分配新对象时可以丢弃这个PLAB块,申请一个新的PLAB,所以这个值越大分配的效率越高,内存浪费也越严重;这个参数和TLABRefillWasteFraction类似。
  • 参数G1EagerReclaimHumongousObjects,默认值为true,表示在YGC时收集大对象;有应用测试发现YGC时回收大对象会引起性能问题,如果遇到可以关闭选项。
  • 参数G1EagerReclaimHumongousObjectsWithStaleRefs,默认值为true,表示在YGC时判定哪些大对象分区可以收集,如果为true表示当时大对象分区RSet的引用关系数小于G1RSetSparseRegionEntries(默认值为0)可以尝试收集,如果为false则只有RSet中的引用数为0才会收集。

伟大的卷发
4 声望4 粉丝

日拱一卒,以求寸进