
本文深入剖析 JVM 在堆空间未耗尽(如配置 594MB 最大堆,但仅使用 351MB)时仍抛出 java.lang.OutOfMemoryError: Java heap space 的典型现象,揭示 G1 垃圾收集器在大对象分配、内存碎片与回收时机约束下的内在机制。
本文深入剖析 jvm 在堆空间未耗尽(如配置 594mb 最大堆,但仅使用 351mb)时仍抛出 `java.lang.outofmemoryerror: java heap space` 的典型现象,揭示 g1 垃圾收集器在大对象分配、内存碎片与回收时机约束下的内在机制。
在分析您提供的 G1 GC 日志时,一个关键矛盾浮现:JVM 配置了 固定堆大小(Min/Initial/Max = 594M),最终 Full GC 后存活对象仅占 351M,但程序仍以 OutOfMemoryError: Java heap space 崩溃。这并非 JVM “未使用全部堆”,而是 可用连续内存不足导致分配失败——本质是 内存碎片化 + 大对象(Humongous Object)分配失败。
? 核心原因:Humongous Allocation 触发 OOM
G1 将大于 Region Size 一半 的对象视为 Humongous 对象(本例中 Region Size = 1M,故 ≥ 512KB 即为 Humongous)。这类对象必须分配在连续的 Humongous Regions 中,且不能跨 Region 存储。
从日志可清晰追踪该路径:
[06:02:07.219] GC(25) Pause Young (Concurrent Start) (G1 Humongous Allocation) 395M->394M(594M) [06:02:07.786] GC(30) Pause Young (Normal) (G1 Humongous Allocation) 522M->523M(594M) [06:02:07.819] GC(31) Pause Full (G1 Compaction Pause) 523M->351M(594M) [06:02:08.596] GC(32) Pause Full (G1 Compaction Pause) 351M->343M(594M)
- GC(25) 和 GC(30) 明确标记为 (G1 Humongous Allocation),表明应用正在尝试分配大对象;
- 尽管 GC 后老年代(Old regions)从 250→278→303→329→346 持续增长,Humongous regions 更是从 120→173→173→173→173 稳定高位,说明大量大对象长期驻留;
- 关键线索在 GC(31) 和 GC(32):两次 Full GC(G1 Compaction Pause)均未能将堆压缩至可容纳下一个 Humongous 分配的程度——GC(31) 后剩余 351M,GC(32) 后仅减至 343M,但内存布局已高度碎片化,无法凑出足够连续的 Humongous Region。
此时,当新大对象(如 new byte[256 * 1024 * 1024])请求分配时:
- G1 检查空闲 Region,发现虽有约 240MB 总空闲(594−351),但无连续的 ≥1 个完整 Region(1MB)可用于 Humongous 分配(实际需多个连续 Region);
- 年轻代 GC 无法回收老年代大对象;
- Concurrent Mark Cycle(GC(26))因内存压力过大而中止(Concurrent Mark Abort),混合回收(Mixed GC)失效;
- JVM 别无选择,抛出 OOM —— 不是“没内存”,而是“没合适形状的内存”。
✅ 验证与诊断方法
-
监控 Humongous 分配频率
启用详细 GC 日志,重点关注含 Humongous Allocation 或 Humongous regions 的行:-Xlog:gc*,gc+heap=debug,gc+ergo*=trace
-
识别大对象来源
使用 JFR(Java Flight Recorder)录制堆分配事件:java -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp
在 JDK Mission Control 中分析 Allocation in new TLAB / Allocation outside TLAB 事件,按 size 排序定位 >512KB 的分配点。
检查内存碎片指标
GC 日志中 Humongous regions 数量持续增长且不下降,是严重碎片信号;Old regions 与 Humongous regions 比例失衡(如本例 Humongous 占 173/594 ≈ 29%)需警惕。
⚙️ 解决方案与调优建议
| 方案 | 操作 | 说明 |
|---|---|---|
| 增大 Region Size | -XX:G1HeapRegionSize=2M 或 4M | 减少 Humongous 对象数量(原 512KB→1MB 才算),降低碎片概率。⚠️ 需权衡:过大 Region 降低 GC 精度。 |
| 主动触发混合回收 | -XX:G1HeapWastePercent=5(默认5%) -XX:G1MixedGCCountTarget=8 |
降低启动混合回收的阈值,更早清理老年代大对象。 |
| 限制 Humongous 分配 | 代码层避免 byte[]、char[] 等超大数组;改用流式处理或内存映射文件(MappedByteBuffer)。 | 根本性规避问题。 |
| 升级 JDK 版本 | JDK 17+ 已优化 Humongous 回收逻辑,JDK 21 引入 ZGC/Shenandoah 可彻底消除此问题。 | 新 GC 算法对大对象更友好。 |
? 总结
JVM 报 OutOfMemoryError: Java heap space 时,堆内存总量未用尽绝非异常,而是 G1 内存管理模型的固有特性体现。当 Humongous 对象堆积导致内存碎片化,即使总空闲空间充足,也无法满足连续内存分配需求。诊断应聚焦 GC 日志中的 Humongous Allocation 标记、Humongous regions 变化趋势及并发标记中止原因;调优需结合 Region Size 调整、混合回收策略强化与应用层大对象治理。记住:在 G1 中,“有多少内存”不如“内存是否连贯可用”更重要。










