G1垃圾回收算法概述

垃圾回收都是基于分区进行的。 G1 在实现垃圾回收时一共提供了 3 种回收的方法,分别是新生代回收(称为 Young GC YGC ),混合回收(称为 Mixed GC ),全回收(称为 Full GC FGC )。这 3 种垃圾回收触发的时机通常如下:

1.  应用程序分配对象时,发现内存不足,触发 YGC

2.  YGC 执行中,判断整体内存使用是否大于一定的阈值,如果大于启动并发标记;在并发标记完成后,当下一次启动垃圾回收称为 Mixed GC ,在 Mixed GC 执行过程中不仅回收新生代分区,同时也回收部分老生代分区;

3.  在用程序分配对象时,发现内存不足,触发 YGC 或者 Mixed GC ;垃圾回收结束后再次尝试分配对象,如果内存还不足,此时将触发 FGC

在整个 JVM 运行过程中,还可以通过外部命令或者代码触发垃圾回收。

1 Young GC概述

G1 对于 YGC Mixed GC 的回收采用的复制算法。简单可以总结为:从根出发,标记活跃对象,并将活跃对象复制到新的分区中。在实际实现中, G1 为了提高回收效率,将整个回收过程进行了细分,大体上拆分为:并行执行部分和串行执行部分。首先 G1 针对多个根进行并行的标记复制,然后再进行串行的标记复制。最早 G1 把引用处理也放在串行执行部分中,后来 G1 对引用处理做了优化,也可以并行执行,但是引用处理并不能直接放入到并行执行部分(因为引用处理必须在多个根并行执行完成之后才能执行)。所以这里把串行执行部分称为其他部分,在其他部分执行中以串行执行为主,有个别的任务的可以并行执行。

YGC 的执行顺序来看一下整个收集过程的主要步骤:

1.  进行收集之前需要 执行暂停。

2.  选择 分区回收集合(称为 CSet ,对于 YGC 来说整个新生代分区 都会被放入到 CSet

3.  进入并行任务处理

1)  根扫描并处理;处理过程会把根直接引用的对象复制到新的 Survivor 区,然后把被引用对象的 成员变量 入栈等待后续的复制处理

2)  处理老生代分区到新生代分区的引用;首先会 把程序运行过程中需要记录的代际关系引用信息进行保存(代际引用关系通过数据结构 RSet 来存储, G1 中还存在单代的引用处理线程) ,然后从 RSet 出发,把 RSet 所在卡表对应的分区内存块中所有的对象都认为是根,把这些根引用到的对象复制到新的 Survivor 区,然后把被引用对象的 成员变量 入栈等待后续的复制处理

3) JIT 代码扫描

4)  根据栈中的对象,进行深度递归遍历复制对象

4.  下面是其他任务处理,其他任务大部分都是串行执行

1) JIT 代码位置更新,在并行任务中已经对代码进行了扫描和复制,这里会更新相关指针所指向的位置

2)  引用处理,即把引用中使用的存活对象也要复制到新的分区

3)  字符串去重优化回收,这个是 JDK8 G1 新引入的功能。是为了优化字符串使用的效率

4)  清除卡表,就是把全局卡表中已经处理过的分区对应的卡表清空

5) JIT 代码回收,代码已经可以回收,实际上是删除相关的引用

6)  转移 失败处理,主要的工作就是恢复对象头;

7)  引用再处理,把引用中还活着的对象放入到引用队列中,这个是和引用特殊的设计有关

8)  进行 Redirty ,这个主要工作就是重构 分区对象的引用关系。

9)  释放 回收集合 CSet 中分区对应的内存 ,即把这些分区放入到自由列表 Free List ,供后续使用,这里的后续指的是对象分配时如果需要新的分区,可以直接从 Free List 获取。

10)  尝试大对象回收

11)  尝试扩展内存,根据 内存的使用情况判断 是否可以扩展

12)  调整新生代分区的数目等;主要是根据 GC 的执行时间和目标停顿时间预测下次可能发生垃圾回收能接受的最大分区数。

2 Mixed GC 概述

Mixed GC 包括两个部分,并发标记和垃圾回收。当标记完成后,垃圾回收过程和 YGC 几乎完全相同,唯一的区别就是在回收的时候,不仅仅回收新生代分区,同时还回收一部分老生代分区(可能需要多次混合回收才能把老生代所有的分区回收完成)。所以着重了解一下并发标记的过程。

并发标记是指并发标记线程和应用程序线程同时运行,它有四个典型的子阶段:初始标记子阶段、并发标记子阶段、再标记子阶段和清理子阶段。

1 )初始标记子阶段

负责标记所有从根集合被直接可达的对象,根集合是对象图的起点,初始标记需要将应用程序线程暂停掉,也就是需要 暂停一切 。在混合回收中的初始标记子阶段和新生代的初始标记几乎一样。实际上混合回收的初始标记子阶段是借用了新生代回收的结果,即新生代垃圾回收后的新生代 Survivor 分区作为根,所以混合回收一定发生在新生代回收之后,且不需要再进行一次初始标记。这就是所谓的 借道

注意实际上除了YGC发生后的 Survivor 作为并发标记的根之外,并发标记还有一些根是在 YGC 中识别的。

2 )并发标记子阶段

YGC 执行结束之后,如果发现满足并发标记的条件之后,并发线程就开始进行并发标记。根据新生代的 Survivor 分区开始并发标记。并发标记的时机是在 YGC 后,只有内存消耗达到一定的阈值后,才会触发。在 G1 中这个阈值通过参数 InitiatingHeapOccupancyPercent 控制( JDK8 默认值是 45 ,表示的是当已经分配的内存加上本次将分配的内存超过内存总容量的 45% 就可以开始并发标记 ,在 JDK 11 中对这一参数的含义又做了调整 )。多个并发标记线程启动,每个线程每次只扫描一个分区,从而标记出存活对象。在标记的时候还会计算存活对象的数量,同时会计算存活对象所占用的内存大小,并计入分区空间。

并发标记子阶段会对所有分区的对象进行标记。这个阶段并不需要 STW ,故标记线程和应用程序线程并发运行。 为了保证并发标记算法的正确, G1使用 Snapshot-At-The-Begining SATB )算法进行并发标记。

3 )再标记子阶段

再标记是最后一个标记阶段。在该阶段中, G1 需要 暂停一切 ,找出所有未被访问的存活对象,同时完成存活内存数据计算。引入该阶段的目的,是为了能够达到结束标记的目标。要结束标记的过程,要满足三个条件:

  • 从根( Survivor )出发并发标记子阶段已经标记出所有的存活对象。

  • 标记栈是空的。

  • 所有的引用变更对象都被处理了; 这里的引用变更对象包括新增空间分配的对象和引用变更对象,新增空间所有对象都认为是活跃的(即便是对象已经死亡也没有关系,在这种情况下只是增加了一些浮动垃圾),引用变更处理的对象通过一个队列记录,在该子阶段会处理这个队列中所有的对象。

4 )清理子阶段

再标记子阶段之后是清理子阶段,该子阶段也需要 暂停一切 。清理子阶段主要执行以下操作:

  • 统计存活对象,统计的结果将会用来排序分区,以用于下一次的垃圾回收时分区的选择。

  • 交换标记位图,为下次并发标记准备。

  • 把空闲分区放到空闲分区列表中; 这里的空闲分区指的是全都是垃圾对象的分区; 如果分区还有任何活跃对象都不会释放,真正释放的动作是在混合回收中。

该阶段比较容易引起误解的地方在于,清理子阶段并不会清理垃圾对象,也不会执行存活对象的拷贝。也就是说,在极端情况下,该阶段结束之后,空闲分区列表将毫无变化, JVM 的内存使用情况也毫无变化。

YGC Mixed GC G1 种最为常见的垃圾回收方式, 整个 G1 垃圾回收的活动图如图所示。

3 Full GC概述

FGC是整个程序运行过程中需要尽量避免发生。 FGC 的发生将导致应用程序停顿时间不可控。所以这里简单的介绍一下 FGC

FGC 采用的标记清除算法,在 JDK10 之前, G1 FGC 采用的串行实现,从 JDK10 开始, FGC 被优化成并行执行。串行和并行的实现基本类似,唯一的不同是,在串行执行时标记清除是针对整个内存,而在并行执行时,多个并行执行的线程按照分区来标记清除。

标记清除算法,主要分为 4 步:

1.  标记活跃对象

2.  计算新对象的地址

3.  把所有的引用都更新到新的地址上

4.  移动对象。

4 G1中Top10参数

在JDK8中 G1 共有 41 个可配置的生成参数,可以在 JVM 启动时调整相关的参数从而保证 JVM 运行达到最优。这里提供工作中最常用的 10 个参数:

  • G1HeapRegionSize ,设置分区大小。

  • G1ReservePercent ,设置保留空间,用于 YGC 时对象晋升时使用。

  • ParallelGCThreads ,设置垃圾回收时并行工作线程的个数。

  • G1ConcRefinementThreads ,设置并发引用工作线程的个数。

  • ConcGCThreads ,设置并发工作线程的个数。

  • I nitiatingHeapOccupancyPercent ,设置并发标记启动的条件。

  • G1MixedGCCoun tTarget ,设置混合回收时,一次回收老生代分区的比例。

  • G1HeapWastePercent ,判断是否启动混合回收,当回收分区集合中可用的内存大于该比例才会被启动。

  • G1UseConcMarkReferenceProcessing ,是否允许并行的处理引用

  • G1MixedGCLiveThresholdPercent ,设置混合回收时,当分区中可用的内存大于该比例才会被回收

关于初始标记中根的介绍、并发标记的SATB算法以及参数的具体介绍可以参考《 JVM G1 源码分析和性能调优》。

推荐语:资深Java工程师撰写,来自一线实践经验的结晶,详细分析G1的基本运行原理以及调优方法,讲解细腻,图示丰富,可帮助Java工程师深入理解垃圾回收技术 

点击链接了解详情并购买

关于作者: 彭成寒 高级Java工程师,目前主要从事风控系统设计、算法建模、大数据处理等工作。有超过10年的Java和C++开发经验。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章