年轻代gc由堆内存使用率触发而非eden填满,卡顿主因是initial marking阶段stw扫描根对象及检查survivor与老年代跨区引用,rset未热或dirty card queue积压导致耗时突增。

年轻代GC怎么触发,又为什么总卡在初始标记?
年轻代GC(Young GC)不是等Eden填满才动,而是当已用堆内存达到阈值(默认 -XX:G1NewSizePercent=5、-XX:G1MaxNewSizePercent=60)就启动。它真正让人感知“卡顿”的地方,是 Initial Marking 阶段——这个阶段必须 STW,且会扫描所有根对象(包括线程栈、JNI引用、全局变量),但关键在于:它还会同步检查哪些 Survivor 区里有对象被老年代引用,这部分依赖 Remembered Set 的更新状态。
常见错误现象:GC pause (G1 Evacuation Pause) (young) 耗时突然变长,尤其在应用刚启动或突发流量后。这不是因为 Eden 太大,而是因为 RSet 还没热起来,或者 Dirty Card Queue 积压太多没及时处理,导致 Initial Marking 被迫做额外工作。
- 确保
-XX:G1ConcRefinementThreads足够(建议设为 CPU 核数的 1/4~1/2),否则并发清理卡表不及时 - 避免在单次 Young GC 中回收过多 Region:可通过
-XX:MaxGCPauseMillis=200让 G1 自动限速,比硬调-XX:G1NewSizePercent更稳 - 如果看到大量
Root Region Scan日志,说明 Survivor 到老年代的跨区引用多,考虑适当提高-XX:G1MixedGCCountTarget分摊压力
混合GC什么时候来,又为什么老年代Region越收越慢?
混合GC(Mixed GC)不是按时间来的,而是看老年代占用率是否越过 -XX:InitiatingHeapOccupancyPercent(默认 45%)。一旦触发,G1 就会从老年代中挑一批“回收价值高、成本低”的 Region 加入回收集——所谓“价值”,就是 Region 里垃圾占比高;所谓“成本”,主要指存活对象复制开销。
容易踩的坑:明明老年代用了 60%,却迟迟不见 Mixed GC;或者来了之后连续多次,每次只清掉一两个 Region,GC 频率飙升。这通常是因为 G1 发现候选 Region 的存活对象太多(比如缓存长期驻留、大对象未及时释放),不敢贸然回收,转而反复尝试,直到最后退化成 Full GC。
- 监控
G1-Evacuation-Info日志里的reclaimable字段,低于 20% 的 Region 基本不会被选进混合回收集 - 若应用有明确生命周期的大对象(如报表导出临时 buffer),主动用
System.gc()不解决问题,反而加剧碎片;应改用池化或手动ByteBuffer.clear()等可控释放方式 -
-XX:G1HeapWastePercent=5是个隐藏开关:当已标记但无法回收的垃圾占堆超该比例,G1 才愿意扩大混合回收范围;默认 5%,太保守可调至 10
并发标记周期卡在 Final Marking,是不是内存不够?
不是内存不够,是 Remembered Set Logs 合并太重。Final Marking(也叫 Remark)阶段要停掉所有应用线程,把所有线程本地的 RS Log 批量刷进全局 RSet,同时还要处理 SATB(Snapshot-At-The-Beginning)写屏障记录的并发修改。如果这段时间里业务正好批量更新大量跨代引用(比如刷新缓存、重组对象图),Log 体积爆炸,就会拖慢整个阶段。
典型表现:GC pause (G1 Remark) 耗时远超 Initial Marking,甚至达数百毫秒;JVM 日志里伴随大量 Processed <n> cards</n> 输出。
- 调大
-XX:G1RSetUpdatingPauseTimePercent=10(默认 10),让并发更新线程多占点 CPU 时间片,减少 Log 积压 - 避免在标记周期高峰(即 Concurrent Marking 阶段)执行全量缓存 reload 或大批量 ORM 查询,这类操作极易触发密集写屏障
- 启用
-XX:+G1UseAdaptiveIHOP(JDK 10+ 默认开启),让 G1 动态学习 IHOP(Initiating Heap Occupancy Percent)阈值,比固定设 45% 更适应真实负载波动
Region 大小怎么设,设错会有什么后果?
Region 大小由 -XX:G1HeapRegionSize 控制,取值范围是 1M~32M,且必须是 2 的幂。它不是越大越好,也不是越小越细粒度——设错直接决定 Humongous 对象判定、RSet 内存开销、以及 GC 并行度上限。
后果很实在:Region 设成 4M,一个 3.5M 的 byte[] 就算普通对象;设成 2M,它立刻变成 Humongous 对象,单独占 2 个连续 Region,且只能在 Full GC 或下一次 Mixed GC 末期才回收,极易引发内存浪费和延迟毛刺。
- 估算公式:目标最大对象大小 ÷ 2,向上取最近 2 的幂。例如业务最大缓存块约 8MB → 选 4M Region
- Region 数量不能超 2048 个(硬限制),所以堆越大,Region 越得设大些。4GB 堆配 2M Region = 2048 个,刚好踩线;再大就必须升到 4M
- 别迷信“小 Region 更精准”——每个 Region 配套 RSet 至少占几 KB,Region 越多,RSet 总开销越高(可达堆的 10%~20%),GC 元数据压力反而上升
真正难的从来不是记住几个阶段名字,而是看懂 GC 日志里那行 [G1Ergonomics (Mixed GCs) add old region to collection set] 后面跟着的 Region 地址,然后反查它为什么被选中、里面到底活了多少对象。这些细节不暴露在配置里,只藏在每次回收的实际决策中。








