【JVM笔记】G1回收器垃圾回收过程

目录

分区Region:化整为零

设置H的原因

G1回收器垃圾回收过程

Remembered Set

过程一:年轻代GC

过程二:并发标记过程

过程三:混合回收

过程四:Full GC


分区Region:化整为零

使用 G1 收集器时,它将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32MB 之间,且为 2 的 N 次幂,即1MB, 2MB, 4MB, 8MB, 16MB, 32MB。可以通过 -XX:G1HeapRegionSize 设定。所有的 Region 大小相同,且在 JVM 生命周期内不会被改变

虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。通过 Region 的动态分配方式实现逻辑上的连续

一个 Region 有可能属于 Eden,Survivor 或者 Old / Tenured 内存区域。但是一个 Region 只可能属于一个角色。图中的 E 表示该 Region 属于 Eden 内存区域,S 表示属于 Survivor 内存区域,O表示属于 Old 内存区域。图中空白的表示未使用的内存空间

G1 垃圾收集器还增加了一种新的内存区域,叫做 Humongous 内存区域,如图中的 H 块。主要用于存储大对象,如果超过 1.5 个 Region,就放到 H

设置H的原因

对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放大对象。如果一个 H 区装不下一个大对象,那么 G1 会寻找连续的 H 区来存储。为了能找到连续的H 区,有时候不得不启动 Full GC。G1 的大多数行为都把 H 区作为老年代的一部分来看待

G1回收器垃圾回收过程

G1 GC 的垃圾回收过程主要包括如下三个环节:

年轻代 GC(Young GC)

老年代并发标记过程(Concurrent Marking)混合回收(Mixed GC)

如果需要,单线程、独占式、高强度的 Full GC 还是继续存在的。它针对 G C的评估失败提供了一种失败保护机制,即强力回收

应用程序分配内存,当年轻代的 Eden 区用尽时开始年轻代回收过程;G1 的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC 暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到 Survivor 区间或者老年区间,也有可能是两个区间都会涉及

当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程

标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC 从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的 G1 回收器和其他 GC不同,G1 的老年代回收器不需要整个老年代被回收,一次只需要扫描 / 回收一小部分老年代的Region 就可以了。同时,这个老年代 Region 是和年轻代一起被回收的

举个例子:一个 web 服务器,Java 进程最大堆内存为 4G,每分钟响应 1500 个请求,每 45 秒钟会新分配大约 2G 的内存。G1 会每 45 秒钟进行一次年轻代回收,每 31 个小时整个堆的使用率会达到 45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收

Remembered Set

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

一个 Region 不可能是孤立的,一个 Region 中的对象可能被其他任意 Region 中对象引用,判断对象存活时,是否需要扫描整个 Java 堆才能保证准确呢

在其他的分代收集器,也存在这样的问题(而 G1 更突出)

回收新生代也不得不同时扫描老年代

这样的话会降低 Minor GC 的效率

解决方法:

无论 G1 还是其他分代收集器,JVM 都是使用 Remembered Set 来避免全局扫描

每个 Region 都有一个对应的 Remembered Set

每次 Reference 类型数据写操作时,都会产生一个 Write Barrier 暂时中断操作

然后检查将要写入的引用指向的对象是否和该 Reference 类型数据在不同的 Region(其他收集器;检查老年代对象是否引用了新生代对象)

如果不同,通过 CardTable 把相关引用信息记录到引用指向对象的所在 Region 对应的 Remembered Set 中 

当进行垃圾收集时,在 GC 根节点的枚举范围加入 Remembered Set ; 就可以保证不进行全局扫描,也不会有遗漏

过程一:年轻代GC

JVM 启动时,G1 先准备好 Eden 区,程序在运行过程中不断创建对象到 Eden 区,当 Eden 空间耗尽时,G1 会启动一次年轻代垃圾回收过程

年轻代垃圾回收只会回收 Eden 区和 Survivor 区

YGC 时,首先 G1 停止应用程序的执行(Stop-The-World),G1 创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代 Eden 区和 Survivor 区所有的内存分段

第一阶段,扫描根

根是指 static 变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同 RSet

记录的外部引用作为扫描存活对象的入口

第二阶段,更新 RSet

处理 dirty card queue 中的 card,更新 RSet。此阶段完成后,RSet 可以准确的反映老年代对所在的内存分段中对象的引用

第三阶段,处理RSet

识别被老年代对象指向的 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象

第四阶段,复制对象

此阶段,对象树被遍历,Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段,Survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加 1,达到阀值会被会被复制到 Old 区中空的内存分段。如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间

第五阶段,处理引用

处理 Soft,Weak, Phantom, Final, JNI Weak 等引用。最终 Eden 空间的数据为空,GC 停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片

过程二:并发标记过程

初始标记阶段

标记从根节点直接可达的对象。这个阶段是 STW 的,并且会触一次年轻代 GC

根区域扫描(Root Region Scanning)

G1 GC 扫描 Survivor 区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC 之前完成

并发标记(Concurrent Marking)

在整个堆中进行并发标记(和应用程序并发执行),此过程可能被 youngGC 中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)

再次标记(Remark)

由于应用程序持续进行,需要修正上一次的标记结果。是 STW的。G1 中采用了比 CMS 更快的初始快照算法:snapshot-at-the-beginning(SATB)

独占清理(cleanup,STW)

计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是 STW 的。

这个阶段并不会实际上去做垃圾的收集

并发清理阶段

识别并清理完全空闲的区域

过程三:混合回收

当越来越多的对象晋升到老年代 oldregion 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 OldGC,除了回收整个 Young Region,还会回收一部分的 Old Region。 这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些OldRegion 进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是 Mixed GC 并不是Full GC

并发标记结束以后, 老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分 8 次(可以通过 -XX: G1MixedGCCountTarget设置)被回收

混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor 区内存分段。混合回收的算法和年轻代回收的算法完全一样, 只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程

由于老年代中的内存分段默认分 8 次回收,G1 会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为 65%,意思是垃圾占内存分段比例要达到 65% 才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间

混合回收并不一定要进行 8 次。有一个阈值 -XX: G1HeapWastePercent,默认值为 10%,意思是允许整个堆内存中有 10% 的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于 10%,则不再进行混合回收。因为 GC 会花费很多的时间但是回收到的内存却很少

过程四:Full GC

G1 的初衷就是要避免 Full GC 的出现。但是如果上述方式不能正常工作,G1 会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长

要避免 Full GC 的发生,一旦发生需要进行调整。什么时候会发生 Full GC 呢?比如堆内存太小,当 G1 在复制存活对象的时候没有空的内存分段可用,则会回退到 full gc,这种情况可以通过增大内存解决

导致 G1 Full GC 的原因可能有两个:

1. Evacuation 的时候没有足够的 to-space 来存放晋升的对象

2. 并发处理过程完成之前空间耗尽

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java小白。。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值