G1垃圾回收器
在 GC 的选择上,同样是“没有银弹”,不同的收集器有着各自的特点和适用场景,即使是 Epsilon 也会在特定场合下发挥作用。我们应针对不同的业务特征和系统情况选择最合适的垃圾回收器,而不是一味求新。
1、最大堆大小
G1管理的最大堆大小为64G。每个Region的大小通过 -XX:G1HeapRegionSize 来设置,大小为 1~32MB ,默认最多可以有2048个Region,G1能管理的最大堆内存是 32MB*2048=64G 。
使用G1垃圾回收器最小堆内存应为 1MB*2048=2GB ,低于此值建议使用其它垃圾回收器。
2、Region大小
Region大小为 1~32MB ,具体取值有1MB、2MB、4MB、8MB、16MB、32MB,Region大小优化与大对象有关,当对象占用内存超过Region的一半时将被视为大对象。
被标记为大对象将不利于垃圾回收。
:如果堆空间(内存)大的时候,每次进行「垃圾回收」都需要对一整块大的区域进行回收,那收集的时间是不好控制的
而划分多个小区域之后,那对这些「小区域」回收就容易控制它的「收集时间」了。
从上一次我们聊CMS回收过程的时候,同样讲到了Minor GC,它是通过「卡表」(cart table)来避免全表扫描老年代的对象
因为Minor GC 是回收年轻代的对象,但如果老年代有对象引用着年轻代,那这些被老年代引用的对象也不能回收掉
同样的,在G1也有这种问题(毕竟是Minor GC)。CMS是卡表,而G1解决「跨代引用」的问题的存储一般叫做RSet
只要记住,RSet这种存储在每个Region都会有,它记录着「其他Region引用了当前Region的对象关系」
对于年轻代的Region,它的RSet 只保存了来自老年代的引用(因为年轻代的没必要存储啊,自己都要做Minor GC了)
对于老年代的 Region 来说,它的 RSet 也只会保存老年代对它的引用(在G1垃圾收集器,老年代回收之前,都会先对年轻代进行回收,所以没必要保存年轻代的引用)
G1 的分代模型
G1 也分为年轻代和年老代,但不是固定划分,而是每个 Region 根据运行情况动态划分。
G1 还有一个特殊的区域叫 Humongous,G1 将超过了一个 Region 容量一半的大对象,都存放在 Humongous 区域中,如果对象超过了 Region 大小,则存放在 N 个连续的 Humongous Region 中。G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待。
TAMS(Top at mark start)
为了保证垃圾回收过程中的同时 Region 也能够被使用,G1 为每一个 Region 设计了两个名为 TAMS 的指针,分别是 Previous TAMS(PTAMS)、Next TAMS(NTAMS)。在并发标记阶段开始前,TAMS 指针指向 Region 内占用内存的边界。在并发标记阶段中,G1 默认指针之上的对象为存活对象不去进行标记,而对象分配时,用户线程直接在指针之上分配。这就保证了扫描行为和对象分配互不干扰。
G1 如何判定 Region 的“价值”
G1 运行期间会收集每个 Region 的价值信息,比如回收耗时、记忆集的脏卡数量等,通过计算得出每个 Region 回收的性价比。G1 的停顿预测模型就是通过这些信息,找出在用户预期时间内获得更高回收收益的 Region 组合。
Remembered Sets
G1 堆中的每一个 Region 都有一份Rememberd Set
,也叫RSet
,它的作用就是为每一个 Region 记录哪些 Region 对其含有引用。
RSet 的更新需要线程同步处理,由于对象引用变更非常频繁,如果同步写卡表消耗非常大,所以通常会把更新信息存入队列中再异步更新 RSet,这个队列就叫Dirty Card Queue
。
G1 的垃圾回收过程
当 Eden 中无法分配对象时,触发 Young GC。
当年老代占比到达 45%时,等待下一次 Young GC 时进行并发标记。
并发标记结束后马上执行 Mixed GC。
当 Mixed GC 对内存的清理速度赶不上分配新对象的速度时触发 Full GC,G1 的 Full GC 将使用单线程(JDK11 后改为多线程)执行标记整理算法,所以耗时巨大。
G1 的 Young GC
触发时机
当 JVM 无法在 Eden 区分配对象时。
回收范围
Eden 区和 Survivor 区
运行过程(所有阶段均 STW)
1. 根扫描
将所有 Eden 区中的 GC Root 和 RSet 记录的外部引用作为扫描存活对象的入口。
2. 更新 RSet
通过 Dirty Card Queue 中的 card 更新 RSet,保证 RSet 能准确反应老年代对该 Region 是否存在引用。
3. 处理 RSet
将 Eden 区中被 RSet 指向的对象标记为存活对象。
4. 对象复制
判断存活对象的年龄,如果未达到“阈值”,则复制到一个 Surviver 区中,否则复制到 Old 区中。如果 Surviver 空间不够,则将部分对象直接复制到 Old 区中。
5. 处理引用
处理软引用、弱引用、虚引用等,最终清空全部 Eden 区。这时清理过的内存空间没有内存碎片。
G1 的 Mixed GC
触发时机
年老代占用空间超过整个堆的 45%(可通过参数-XX:InitiatingHeapOccupancyPercent
进行设置)
事实上,并不会立刻触发,而且等待下一次 Young GC,同步进行初始标记步骤。
回收范围
被并发标记过的 Region,这些 Region 是 G1 通过价值测算动态选中的。
运行过程
1. 初始标记(Initial Marking)[STW]
标记 GC Roots 直接关联的对象,并修改 TAMS 指针的值。值得注意的是,这一阶段并不单独执行,而是在 Minor GC 时同步完成。所以实际上这个阶段没有额外停顿。
2. 并发标记(Concurrent Marking)
与用户线程并发执行,顺着 GC Root 递归标记。标记完成后,重新扫描 SATB 记录的有引用变动的对象。如果这时发现空的 Region 则直接将其清空。
3. 重新标记(Remark)[STW]
由于并发标记是并发执行,并发标记结束后,仍然存在少量的引用变动的对象,所以在这个阶段可以 STW 来处理这部分遗留的对象。并且开始计算所有 Region 的活跃度。
4. 清理(Clean Up)[STW]
根据用户期望的停顿时间来制定回收计划,选择全部是非存活对象的 Old 区和回收收益较高的 Region 加入回收集。清空记忆集。重置已经被清理的空的 Region(这一步是非 STW 的)。
5. 拷贝(Coping)[STW]
将回收集其中的存活对象复制到空的 Region 中,最后清空这些旧的 Region。
这个阶段的算法和 Young GC 完全一致,但默认分 8 次执行完成(可由参数-XX:G1MixedGCCountTarget 设置)。所以每次清理的回收集包括 Eden 区、Survivor 区和八分之一的 Old 区。低存活度(垃圾多)的 Region 清理的较快,所以会被 G1 优先回收。
混合回收并不一定要进行 8 次。有一个阈值-XX :G1HeapWastePercent(默认值为 10%),意思是允许整个堆内存中有 10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于 10%,则不再进行混合回收。
优点
G1 相比较之前的垃圾回收器最大的变化是通过化整为零的思路,将堆分为若干个小的 Region 来减少 GC 的范围,从而达到“低延迟”的目的。
并且 G1 的垃圾回收过程采用标记复制的算法,避免了空间碎片化的问题。
缺点
1.内存占用较高,由于 G1 分区比 CMS 更多,每个 Region 都需要建立卡表。其中新生代对象变动频繁,又加大了卡表维护的成本。
2.G1 不仅需要通过写前屏障来更新卡表,还需要写后屏障来跟踪并发时的指针变化以实现快照搜索算法(SATB)。这样虽然相比增量更新算法能够减少并发标记和重新标记阶段的消耗,但是用户程序运行时的计算负载就高了。
3.G1 和 CMS 同样具有“并发回收”的能力,所以垃圾回收的速度如果跟不上用户创建新对象的速度,那么就会触发一个 Full GC 来获取更多内存。通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
这里要提下的是,在G1还有另一个名词,叫做CSet。
它的全称是 Collection Set,保存了一次GC中「将执行垃圾回收」的Region。CSet中的所有存活对象都会被转移到别的可用Region上
最佳实践
1.不要设置年轻代大小年轻代大小应当由 G1 自行控制,设置为固定值将覆盖暂停时间目标
2.暂停时间目标不要过于严苛 G1 为了 Young GC 能够缩短时间需要减少 Eden 区的个数,那么 Young GC 就会更加频繁。Mixed GC 想要达到停顿目标就需要减少回收的垃圾数量,如果回收速度低于新对象分配速度将引起 Full GC。
3.CMS 和 G1 的选择目前在小内存应用上 CMS 的表现大概率仍然要会优于 G1,而在大内存应用上 G1 则大多能发挥其优势,这个优劣势的 Java 堆容量平衡点通常在 6GB 至 8GB 之间。
7 总结
在 GC 的选择上,同样是“没有银弹”,不同的收集器有着各自的特点和适用场景,即使是 Epsilon 也会在特定场合下发挥作用。我们应针对不同的业务特征和系统情况选择最合适的垃圾回收器,而不是一味求新。
1、堆内存
参数 | 默认值 | 说明 | 优化建议 |
---|---|---|---|
MaxGCPauseMillis | 200ms | 最大停顿时间 | |
G1HeapRegionSize | 不设置时启发式推断 | ||
G1NewSizePercent | 5 | 新生代最小百分比 | |
G1MaxNewSizePercent | 60 | 新生代最大百分比 |
2、新生代内存回收
参数 | 默认值 | 说明 | 优化建议 |
---|---|---|---|
ParallelGCThreads | 并行GC线程数,会根据CPU核数推断 | 默认值 | |
MaxTenuringThreshold | 15 | 从新生代晋升到老年代年龄阈值 | |
SurvivorRatio | 8 | Eden和一个Survivor的比例 | |
TargetSurvivorRatio | 50 | Survivor区内存使用率,增大该值会降低到老年代概率 | |
+G1EagerReclaimHumongousObjects | true | 是否在YGC时回收大对象 |
3、混合回收
参数 | 默认值 | 说明 | 优化建议 |
---|---|---|---|
G1MixedGCCountTarget | 8 | 值越大,收集老年代分区越少 | |
G1OldCSetRegionThresholdPercent | 10 | 表示一次最多收集10%的分区 |
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)