JVM深入理解(6)——垃圾回收(6)

由于G1垃圾回收器比较复杂,所以单拉出一篇来细讲。
本文参考了诸多文章:
G1垃圾回收器详解
G1 垃圾回收器
JVM(四) G1 收集器工作原理介绍
三色标记法与读写屏障
G1垃圾收集器详解
以及b站尚硅谷的课程。

G1回收器:区域化分代式

既然已经有了强大GC,为什么发布G1(Gargage First)?
业务越来越庞大、复杂,用户越来越多。没有GC就不能保证应用程序正常进行。经常STW的GC跟不上实际的需求。
是在Java7 update4后引入的新垃圾回收器

目标是:延迟可控的情况下获得尽可能高的吞吐量

为什么叫Garbage First(G1)呢?

  1. 因为G1是一个并行回收器,它把堆内存分隔为很多不相关的区域。使用不同的Region来表示Eden、幸存者0区、幸存者1区、老年代等。
  2. G1 有计划地避免了在堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小,(回收所得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表。每次根据允许的收集时间,优先回收价值最大的Region。
  3. 由于侧重点在于回收垃圾最大量的区间,所以称为:垃圾优先。

面向服务端应用,主要针对配备多核CPU及大容量内存的机器。

是JDK9以后的默认垃圾回收器,取代了CMS及parallel+parallel Old组合,被官方称为全功能垃圾收集器。

特点

  1. 兼顾并行与并发
  2. 分代收集
    • 仍然是分代型垃圾回收器,年轻代依然有Eden区和Survivor区。但从堆结构上看,不要求整个Eden区、年轻代或老年代都是连续的,也不坚持固定大小和固定数量。

在这里插入图片描述

- 堆空间分为若干个区域,包含了逻辑上的年轻代和老年代
- 同时兼顾**年轻代和老年代**。
  1. 空间整合
    • CMS: 标记-清除 算法、内存碎片,若干次GC后进行一次碎片整理
    • G1将内存划分为一个个的region,内存的回收是以region为基本单位,region之间是复制算法(比如从E区放入S区)。但整体上又可以看做是标记-压缩算法。两种算法都避免了内存碎片。有利于程序长时间运行。
  2. 可预测的停顿时间模型(软实时soft real-time)
    G1相对于CMS另一大优势。除了追求低延迟,还能建立可预测的停顿时间 模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集的实际不超过N毫秒(尽可能)。
    • 由于分区,G1可以只选取部分区域进行内存回收,缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
    • G1跟踪各个Region里垃圾堆积的价值大小,维护优先列表,每次根据允许的收集时间,优先回收价值最大的区域。保证高收集效率。

缺点
相比于CMS,还不具有压倒性优势。比如:内存占用和执行负载都比CMS高。所以G1在大内存应用上优势更大。

指令
-XX:G1HeapRegionSize设置每个region的大小,1-32MB之间。默认是堆内存的1/2000
-XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间指标。默认200ms
-XX:ParallelGCThread设置STW工作线程的值,最多为8
-XX:ConcGCThreads设置并发标记的线程数
-XX:InitiatingHeapOccupancyPercent设置并发GC周期的Java堆占用率阈值。

适用场景

服务器端,大内存、多处理器

替换CMS场景:

  1. 50%的堆被活动数据占用;
  2. 对象分配频率和年代提升频率变化很大
  3. GC停顿时间过长

Region

  1. 所有的region大小相同,且在JVM生命周期内不会被改变
  2. 虽然保有新生代老年代的概念,但不再是物理隔离的了,都是一部分region的集合,通过region的动态分配方式实现逻辑上的连续。
  3. 一个region一次只能为一个类型的区域,但在JVM生命周期内可能发生改变。
  4. 新增一个内存区域,称为Humogous内存区域,主要用于存储大对象,如果超过1.5region就放到H

为什么设置H?
对于堆中的大对象,默认直接分配到老年代,但如果是一个短期存在的大对象,就会对垃圾收集器产生负面影响。为了解决这个问题,G1划分了Humongous区,专门存放大对象。如果对象超过一个区域的50%,G1会寻找连续的H区来存储。有时候不得不进行Full GC,G1的大多数行为都把H区作为老年代的一部分看待。

回收细节

主要包括:

  1. 年轻代GC
  2. 年轻代GC+老年代并发标记过程
  3. 混合回收Mixed GC (全部年轻代和部分老年代)
  4. 如果需要,单线程、独占式、高强度的Full GC还是继续存在的。

当Eden区用尽,开始年轻代回收过程,是并行的独占式收集器。多线程、暂停用户线程回收。

当堆内存使用达到一定值(默认45%),开始老年代并发标记过程。
标记完成开始混合回收过程。 从老年代移动存活对象到空闲区间,其也就成了老年代的部分。老年代的G1不需要整个老年代被回收,一次只需要扫描/回收一部分老年代的region。同时,老年代Region和年轻代是一起被回收的。

理解垃圾回收之前

在详解过程前,先要介绍几个结构和方法去1

记忆集

一个对象可能被不同区域的对象引用问题

一个region的对象可能被另一个region中的对象引用,是否需要扫描整个堆?
回收新生代还需要扫描老年代?

所有涉及部分区域收集行为的垃圾收集器,JVM都使用Remenbered Set来避免全局扫描

  1. 给每一个region都分配一个记忆集
  2. 每次reference类型数据写操作时,都会产生一个写屏障暂时中断操作。
  3. 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region
  4. 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remenbered Set中(卡表是RS的一种实现方式,只要卡页内至少有一个对象的字段存在跨带指针,那就将对应卡表的数组元素的值标识为1,称为dirty,没有则标为0)
  5. 当进行垃圾收集时,在GC root的枚举范围加入RS,保证不进行全局扫描,也不会有遗漏。(即筛选出卡表中变脏的元素)
    在这里插入图片描述在这里插入图片描述

G1记忆集在存储结构的本质上是一种哈希表,Key是别的region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。
(这一块其实比较模糊,笔者阅读了多个文章,终于得出一个比较合理的解释:)
每一个region被分成了多个卡页,而记忆集中的每个key,其实就对应于一个region,key对应的value中每一个元素,都对应着一个卡页。这个元素的值是否为1,就表示其是否变脏。
所以存在如图所示的两种对应关系,一种是region的卡页中的对象会指向其它region的卡页中的对象,这时,就要到那个region的记忆集中记录:某个region的卡页x指向我这个region的对象。

RS如何辅助GC?
在YGC时,只需要扫描GC roots 加上 RS里面存在的dirty card,而无需扫描整个Old Generation。
Mixed GC时,老年代中记录了old->old的RS,young->old的引用由扫描S区得到。

三色标记法

白色:表示对象尚未被垃圾收集器访问,所有对象都是在最初时都是白色的。若分析结束仍然为白色,则代表不可达。
黑色:表示对象已经被垃圾回收器访问,且该对象的所有直接引用都被扫描过。如果其它对象引用指向了黑色对象,无须重新扫描一遍。
灰色:已经访问过,但至少存在一个引用没有被扫描过。
在这里插入图片描述

为什么需要保证一致性的快照?
如果用户线程与收集器并发工作,将可能会出现未扫描和已经扫描过的对象更改了引用,导致本来应该存活的对象,误判成了垃圾对象,或者相反。后者可能会产生浮动垃圾,而前者则不能接受。
在这里插入图片描述

两种情况都满足,会产生“对象消失”的问题:

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  2. 赋值器删除了全部从灰色对象到白色对象的直接或间接引用

如果仅有条件1:
相当于对象多了黑色对象的引用

如果仅有条件2:
对象直接变成了不可达对象,成为了垃圾,与扫描结果一致。

条件1,2同时存在:扫描结果为不存在对象,但实际上有黑色对象引用此对象。

由此引出下面两个解决方案

增量更新(Incremental Update)

破坏了条件1

当黑色对象插入新的指向白色对象的引用关系时。就记录下来。
等并发扫描结束之后,再以记录中的黑色对象为根,重新扫描一次。
可以理解为黑色对象一旦插入对白色对象的引用,就变成灰色对象了。

原始快照(SATB)

破坏了条件2

当灰色对象要删除指向白色对象的引用关系时,就将要删除的引用记录下来,扫描结束后,再以记录中的灰色对象为根,重新扫描一次。

可以理解为,无论引用关系删除与否。都按照刚开始扫描那一刻的对象图快照来进行搜索。

以上两种条件只需要选其一破坏即可,即选用增量更新或者原始快照
对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:

  • CMS:写屏障 + 增量更新
  • G1:写屏障 + SATB
  • ZGC:读屏障

写屏障

卡表如何维护?何时变脏?
当其它分代的对象引用了本区域对象时,其对应的卡表元素就应该变脏了。但问题是如何在赋值的那一刻更新卡表呢?

在Hotspot中,通过写屏障(Write Barrier)维护卡表状态。在引用对象赋值时会产生一个环形通知,供程序执行额外动作。赋值的前后都在写屏障的覆盖范畴内。赋值前的叫写前屏障,赋值后的叫写后屏障。许多收集器都使用了写屏障,但G1出现之前,其他收集器都只用到了写后屏障。

void oop_field_store(oop* field, oop new_value) {
  pre_write_barrier(field);             // pre-write barrier: for maintaining SATB invariant
  *field = new_value;                   // the actual store
  post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
}

一旦收集器在写屏障中增加了更新卡表操作,无论更新的是否是老年代对新生代对象的引用,每次只要更新引用,就会产生开销。

写前屏障:
等式左侧对象将修改引用到另一个对象,那么原先引用对象所在分区就会失去一个引用,所以jvm就需要记录丧失引用的对象。但不会立即更新,而是记录这次更新日志,在将来批量处理。
写后屏障
由于左侧对象产生了一个新的引用,所引用对象所在分区的RSet需要被更新。但不会立即更新,而是记录这次更新日志,在将来批量处理。

void pre_write_barrier(oop* field) {
    oop old_value = *field; // 获取旧值
    remark_set.add(old_value); // 记录 原来的引用对象
}

G1通过写前屏障来实现SATB,将变化记录保存在在satb_mark_queue队列中。remark阶段会扫描这个队列,通过这种方式,原来引用指向的对象就会被标记上。

TAMS(Top at Mark Start)

G1为每个region设计了两个名为TAMS的指针,把region中的一部分空间用于并发过程中的新对象分配。G1收集器默认在这个地址上的对象是被隐式标记过的,默认存活,不纳入回收范围。

如果内存分配的速度赶不上回收的速度。G1收集器也会被迫冻结用户线程,导致full gc而产生长时间STW

G1回收过程

Global Concurrent Marking(全局并发标记)

  1. 初始标记:STW。扫描根集合可直达对象,并压入栈中等待后续扫描。它是利用了年轻代收集的STW时间段来完成的初始标记,所以并没有额外的停顿。

  2. (根区域扫描):初始标记结束后,且年轻代也完成对象复制到S区的工作,为了保证标记算法的正确性,所有新复制到S区的对象都需要被扫描并标记为根。这个扫描必须在下一次YGC前完成(并发过程中可能会被年轻代垃圾收集打断),因为每次GC会产生新的存活对象集合。

  3. 并发标记:不断从栈中取出引用,递归扫描堆中的对象图,找出要回收的对象。扫描完成后,还需要重新处理SATB记录下的在并发时有引用变动的对象。

  4. 最终标记:STW,用于清理遗留的SATB记录,同时也进行弱引用处理。

  5. 清理:STW,

    • 在marking bitmap中统计每个region被标记为活的对象有多少。统计结果会用来排序region,用于下次CSet选择
    • 梳理RSet
    • 空闲region放入空闲region列表中
      但并不会清理垃圾对象,也不会执行存活对象的拷贝。
  6. Evacuation, STW

    • 从region中选出若干个region进行回收,被选中的region称为 Collect Set(CSet)
    • 把这些region中的存活对象复制到空闲region中,同时把被回收的region放到空闲列表中
      分解为以下三个行动
    1. 根据日志来更新RS(这个日志应该就是写前屏障所保留下来的日志)
    2. 扫描RS和其余根来确定存活对象
    3. 拷贝存活对象,从2中确定的根触发,沿引用链一直追溯,将存活对象复制到新region。在这个过程中,可能由部分年轻代对象晋升至老年代。

在***深入理解java虚拟机***中,后两步被合并为了一步

G1垃圾回收器本身没有full gc的概念,如果mixed GC无法满足,则会切换到serial old GC来收集整个堆。

总结来看,G1仅有一个阶段是不需要STW的,所以G1并非纯粹地追求低延迟

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值