g1通过动态region实现逻辑分代:每个region可实时标记为eden、survivor、old或humongous,新生代由弹性伸缩的eden+survivor region组成,老年代由自然晋升填充,humongous region专用于≥50%region大小的大对象,需连续物理内存,分配失败源于外部碎片而非总内存不足。

G1收集器不按固定边界划分新生代和老年代,而是把堆切成多个大小相等的Region,每个Region在运行时动态标记为Eden、Survivor、Old或Humongous——这种“逻辑分代、物理混布”的设计提升了内存利用率,但也让大对象分配变得敏感且隐蔽。
Region如何动态扮演不同代角色
每个Region初始为空闲状态,新对象优先分配到Eden类型的Region;当Eden填满触发Young GC时,存活对象被复制到Survivor Region(也可能直接晋升到Old Region);晋升阈值由-XX:MaxTenuringThreshold控制,但实际晋升还取决于Region空间压力和复制成本。关键点在于:所有Region都可被重用,类型随分配行为实时切换,没有永久归属。
- 年轻代由若干连续或离散的Eden+Survivor Region组成,总量在5%~60%堆之间弹性伸缩
- Old Region不预占固定区域,而是由长期存活对象自然填充形成
- Humongous Region一旦分配,就不再参与年轻代回收流程,也不接受晋升,只作为老年代的一部分存在
Humongous对象触发分配失败的真实原因
不是因为堆总内存不足,而是因为找不到足够数量的物理连续空闲Region。比如一个对象大小为4.1MB,而Region设为4MB,则需2个连续Region;若此时堆中虽有10个空闲Region,但最大连续段只有1个,分配立即失败并抛出OutOfMemoryError: Java heap space (large object allocation)。
- JVM对象寻址依赖单一线性地址段,跨非连续Region无法用单一指针+偏移访问
- G1不提供跨Region间接寻址机制,也没有碎片整理能力,只能硬性要求连续
- 这种失败属于外部碎片问题,与普通堆溢出性质不同,jstat显示OU(老年代已用)可能远低于OC(老年代容量)
大对象如何悄悄拖垮GC节奏
Humongous对象跳过Eden,直入老年代,不参与Young GC,只可能在Mixed GC或Full GC中回收。问题在于:它们会加速老年代占用率上升,提前触发并发标记周期,进而频繁启动Mixed GC;更糟的是,若这些对象生命周期短,却因引用残留或标记延迟未能及时回收,就会堆积并挤压后续大对象空间。
- 日志中反复出现humongous allocation或Attempt to allocate humongous object是明确信号
- jstat -gc输出中HU(Humongous used)持续增长、而Mixed GC中humongous region freed占比偏低,说明回收效率差
- JDK 8u40后支持单Region独立回收,但前提是该Region内所有对象完全不可达——SATB标记很难100%精准,实践中仍常整组滞留
调优不能只靠改参数,得从分配源头控制
调整-XX:G1HeapRegionSize能缓解临界值误判(如把默认2MB Region改为4MB,让3MB数组免于Humongous),但治标不治本。真正有效的做法是减少高频、短命的大对象创建。
- 批量数据处理改用流式分块,避免new byte[10*1024*1024]这类硬编码大数组
- 缓存场景优先用ArrayList而非预分配超大数组,它内部扩容策略更可控
- 固定用途缓冲区(如网络包解析)使用对象池(如Netty的PooledByteBufAllocator)复用实例
- 检查Jackson、FastJSON等库是否启用streaming模式或buffer reuse选项











