答案是堆内存空间碎片化导致——虽总使用率不高,但无法找到连续80MB空闲区域分配long[10_000_000];根本原因在于基本类型数组需连续内存,而CMS/Parallel GC易产生老年代碎片,G1对大数组有Humongous Region限制,GC日志和MAT分析可确认该问题。
![如何排查由于基本数据类型数组(如 long[])在堆中分配连续空间失败引发的 oom 空间碎片化难点](https://img.php.cn/upload/article/001/242/473/177979542151822.jpeg)
当 JVM 报出 java.lang.OutOfMemoryError: Java heap space,但堆内存总使用率并不高(比如仅 70%),且频繁出现在分配大数组(如 long[10_000_000])时,极大概率是堆内存**空间碎片化**导致的——即虽有足够总空间,却找不到一块连续的内存块容纳该数组。
为什么 long[] 容易触发碎片化问题
基本类型数组(尤其是 long[]、double[]、byte[])在堆中必须分配**连续内存**。一个 long[10_000_000] 占用约 80 MB(每个 long 8 字节),要求 JVM 在 Eden 或老年代中找到一块 80 MB 的连续空闲区域。而经过多次 GC 后,老年代容易产生大量不连续的小碎片,尤其在使用 CMS 或 Parallel GC 时,碎片积累更明显。
- CMS 不做压缩,长期运行后老年代碎片严重
- Parallel Old 默认开启压缩,但若对象晋升过快或 GC 触发不及时,仍可能在压缩前就失败
- G1 虽以 Region 为单位管理,但大数组(≥ G1HeapRegionSize 的一半)会被分配为 Humongous Region;若连续 Humongous Region 不足,也会直接 OOM
确认是否为碎片化而非真内存不足
关键看 GC 日志和堆转储特征:
- 日志中出现
to-space overflow(Parallel GC)、concurrent mode failure(CMS)或Humongous allocation failed(G1) - 使用
jstat -gc <pid>观察:老年代U(used)不高,但CCU(Compressed Class Space used)正常,FGC频次低,却反复 Full GC 后仍失败 - 用
jmap -histo:live <pid>查看最大对象,若 top 1 是[J(long 数组)且实例数少但单个极大,倾向碎片化 - 生成 heap dump 后用 MAT 打开 → “Leak Suspects” 通常不报泄漏;切换到 “Histogram” → 按 size 排序,看是否有几个超大
[J实例,再右键 “Merge Shortest Paths to GC Roots” 确认是否被合理持有
针对性排查与缓解手段
不能只靠加 -Xmx——碎片不会因堆变大而自动消失,反而可能更难压缩。
- 启用 GC 日志:添加
-Xlog:gc*,gc+heap=debug,gc+metaspace=debug:file=gc.log:time,tags,level(JDK 11+),重点观察每次 GC 后老年代的 最大连续空闲块大小(部分 GC 日志会显式打印,如 G1 的Free space区域统计) - 强制触发堆压缩验证:对疑似环境,在低峰期执行
jcmd <pid> VM.runFinalization+jcmd <pid> VM.gc,再立即尝试分配同尺寸数组;若成功,说明是 GC 延迟或碎片未整理 - 检查是否误用大数组:例如
new long[n]中n来自用户输入或未分页查询结果,应加阈值校验(如if (n > 1_000_000) throw new IllegalArgumentException()) - 改用流式/分块处理:避免一次性加载全量数据,例如用
IntStream.range(0, n).mapToLong(...).toArray()改为分批计算写入磁盘或队列 - 调整 GC 策略:
- JDK 8u2XX+:启用
-XX:+UseG1GC -XX:G1HeapRegionSize=4M(增大 region 大小,减少 Humongous 分配频次) - JDK 11+:考虑
-XX:+UseZGC或-XX:+UseShenandoahGC,它们支持并发压缩,天然缓解碎片问题
- JDK 8u2XX+:启用
预防性设计建议
从代码层规避对连续大内存的强依赖:
- 用
ByteBuffer.allocateDirect()替代超大堆内数组(注意 Direct Memory 也要监控,配-XX:MaxDirectMemorySize) - 对固定结构大数据,考虑内存映射文件(
FileChannel.map()),让 OS 管理物理连续性 - 缓存层避免长期持有大数组引用;使用弱引用容器(如
WeakHashMap)或带 LRU 的本地缓存(Caffeine)自动淘汰 - 在启动参数中加入
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,建立基线 GC 行为画像,便于异常时比对

















