Java虚拟机:G1垃圾回收器

Java虚拟机:G1垃圾回收器
一、 为什么需要 G1化整为零的思想在 G1 出现之前无论是新生代Minor GC还是老年代CMS垃圾回收总是面对一整块“巨大的内存”。这导致STW 时间长扫描全堆或全代非常耗时。内存碎片CMS 使用“标记-清除”算法回收后产生大量不连续空间导致大对象无法分配触发 Full GC。G1 的核心突破区域化Region。G1 将整个堆内存Heap化整为零划分成了大小相同的多个独立子区域Region。这就好比将一整块大的田地划分成了一块块小方格G1 可以针对性地回收某些“脏”的方格而不必每次都犁遍整块地。Region 的大小JVM 启动时自动设定通常为1MB ~ 32MB且必须是 2 的幂。默认会将整堆划分为2048个 Region。内存上限通过参数-XX:G1HeapRegionSizen可指定大小。由于最大只有 2048 个区域所以最大支持32MB * 2048 64GB的堆内存。✨ G1 的五大“杀手锏”特性并行性回收期间多线程并行工作充分利用多核 CPU 缩短 STW。并发性部分回收步骤与应用程序用户线程交替执行不阻塞应用。分代 GC逻辑上虽然物理上不再区分新生代和老年代但逻辑上依然存在 Eden、Survivor 和 Old 区只是这些区域是可以动态切换的。空间整理通过复制算法回收每次 GC 都会压缩空间彻底解决内存碎片问题。可预见性由于分区G1 可以预测停顿时间只选择部分垃圾最多的 Region 进行回收即CSet极大降低了单次 GC 的影响。二、 图解 G1 堆内存布局Region 地图G1 的内存布局不再像传统堆那样是固定的Eden和Old长条而是由无数个Region方格组成 E (Eden)新生代存活率低GC 频繁。 S (Survivor)幸存者区用于存活对象在新生代之间的周转。 O (Old)老年代存放长生命周期对象。 H (Humongous)巨型对象区G1 独有的特殊区域。当一个对象的大小超过单个 Region 的 50%G1 会将其分配在连续的 H 区。坑点提示如果 H 区装不下需要连续拼接多个 H 区。如果找不到连续空间就会导致 Full GC。 核心逻辑这些 Region 并不是固定不变的。在运行过程中一个原本是Eden的区域可能经过回收后变为Old完全取决于需求。这种“逻辑分代、物理分区”的设计也是 G1 灵活性的来源。三、 G1 的两大回收过程详解G1 的回收分为两种核心场景新生代 GC (Young GC)和并发标记混合 GC (Mixed GC)。1. 新生代 GC (Young GC) —— “复制与晋升”触发时机当 Eden 区的空间耗尽时触发 Young GC。操作流程基于复制算法Eden ➡️ Survivor将 Eden 区中存活的对象复制到一个 Survivor 区。Survivor ➡️ Survivor/Old将原 Survivor 中的存活对象根据年龄Age判定移到另一个 Survivor 区或直接晋升到 Old 区如果年龄达标或 Survivor 容量不足。清空 Eden垃圾对象被彻底清理形成连续的内存块。特点这是一个完全 STWStop-The-World的过程但只扫描 Eden 区和相关的 Survivor 区速度极快。2. 并发标记周期与混合 GC (Mixed GC) —— “渐进式清理”目标处理老年代Old 区和巨型区H 区的垃圾降低整体停顿。整个并发标记周期分为 6 个精准步骤重点步骤名称是否 STW核心动作详解①初始标记✅STW标记从GC Roots直接可达的对象。注意这一步会伴随一次新生代 GC 执行标记过程极短。②根区域扫描❌ 并发扫描 Survivor 区直接可达的老年代对象。不能和新生代 GC 同时执行因为新生代会修改 Survivor 区③并发标记❌ 并发最长、最耗时的阶段。GC 线程与应用线程共同运行扫描整个堆查找存活对象。应用线程的变动会通过SATB (Snapshot-At-The-Beginning)算法记录快照防止误判。④重新标记✅STW由于并发标记期间应用还在跑结果可能不准确。这一步会补充修正标记最终确定存活对象。⑤独占清理✅STW统计各个 Region 的存活对象比例进行排序并识别出垃圾最多Garbage-First的区域确定下一次混合回收的CSet集合。⑥并发清理❌ 并发仅清除完全空闲的 Region。 混合回收 (Mixed GC)在并发标记完成后G1 会挑选出之前识别的“高收益” Region即垃圾最多的那几个执行一次混合回收。这个过程也是复制算法会搬运存活对象到新的 Region从而腾出大量空间并顺便完成内存碎片整理。四、 不得已的 Full GC —— 鱼和熊掌的平衡尽管 G1 很强大但世界并非完美。Full GC 触发当内存分配太快并发收集跟不上的时候例如并发标记还没结束老年代就满了G1 不得不降级退化为单线程的 Full GC即完全 STW且速度很慢性能骤降。与 CMS 的相似性和 CMS 类似G1 依然无法 100% 避免全堆停顿特别是在高并发、高频分配的大内存场景下。五、 G1 实战调优参数核心配置清单了解底层原理是为了更好的调优。以下是 G1 最关键的几个参数参数名默认值作用及调优建议-XX:UseG1GC无开启 G1 垃圾收集器。从 JDK 9 起G1 已经是默认垃圾回收器。-XX:MaxGCPauseMillis200ms停顿时间目标。G1 会为了达到这个目标动态调整年轻代大小Eden/Survivor。注意设置过小如 50ms会导致 GC 更频繁吞吐量下降设置过大可能会使 Full GC 风险增加。-XX:ParallelGCThreadsCPU 核心数设置并行回收时的GC 工作线程数。通常不建议修改让 JVM 自动探测即可。-XX:InitiatingHeapOccupancyPercent45触发并发标记的堆占用阈值。当整个堆占用率达到 45% 时开始并发标记周期。⚠️重要警醒此参数设置过大如 50%可能导致堆满而触发 Full GC设置过小如 30%会导致并发标记频繁启动大量占用 CPU拖累业务应用性能。调优黄金法则G1 的目标是“软实时”即MaxGCPauseMillis是一个尽力而为的目标而不是强制标准。在调优时我们应该优先关注吞吐量和Full GC 的频率不要盲目追求极端的低停顿。六、 附G1 典型 GC 日志解析看懂了日志你才能知道 JVM 在底层干了什么。这是一段典型的 G1 Young GC 日志[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0080719 secs] [Parallel Time: 5.1 ms, GC Workers: 10] -- 并发工作线程数量STW耗时 [Eden: 6144.0K(6144.0K)-0.0B(1024.0K) -- Eden区 6MB - 0MB新Eden为1MB Survivors: 0.0B-0.0B -- Survivor 空间变化 Heap: 8262.9K(10.0M)-6516.9K(10.0M)] -- 整个堆从 8.2MB 降低到 6.5MB [Times: user0.00 sys0.00, real0.01 secs]日志解读GC pause (young) (initial-mark)这是一次 Young GC并且伴随了“初始标记”。Heap: ... - ...说明堆内存发生了回收垃圾被清理了。时间开销real0.01 secs(10ms)说明这次 GC 停顿几乎对用户无感知。并发标记结束日志[GC concurrent-root-region-scan-start] -- 开始根区域扫描 (并发) [GC concurrent-root-region-scan-end, 0.0000227s] [GC concurrent-mark-start] -- 开始并发标记 (并发) [GC concurrent-mark-end, 0.0008297s] [GC remark ... 0.0017108 secs] -- 重新标记 (STW) [GC cleanup 7215K-7215K(10M), 0.0012619 secs] -- 清理空闲区域 (并发)总结G1 作为一款现代化的垃圾回收器其核心价值在于“化整为零”的 Region 设计和“暂停时间可控”的混合回收机制。它不仅解决了 CMS 长期存在的内存碎片问题还通过并发与并行极大地降低了 STW 的冲击。但也要记住一切优化都有代价。为了降低停顿G1 牺牲了部分内存空间如额外的记忆集 Remembered Set和 CPU 计算资源。在 64GB 以下的大内存、对响应时间RT敏感、多核 CPU 的服务器环境下G1 是现阶段的最佳选择。