1 运行时数据区域总览(名称 → 私有/共享 → 典型异常)
- 程序计数器(PC) → 线程私有 → (规范未定义 OOME);记录下一条 将执行的字节码指令地址/偏移(执行
native
时可能无意义)。 - 虚拟机栈 → 线程私有 →
StackOverflowError
(栈深超限);OutOfMemoryError
(需要扩栈但内存不足);一帧=局部变量表/操作数栈/动态链接/返回地址。 - 本地方法栈 → 线程私有 → 同上两类异常;为
native
方法服务。 - 堆 → 共享 →
OutOfMemoryError: Java heap space
(或 GC overhead limit exceeded);存放对象实例/数组,GC 主要管理区域。 - 方法区(HotSpot: Metaspace) → 共享 →
OutOfMemoryError: Metaspace
;承载类元数据/运行时常量池/静态方法信息(JIT 代码在 Code Cache)。 - 运行时常量池 → 共享 (属方法区一部分)→
OutOfMemoryError
(常伴随 constant pool 报文);存字面量/符号引用等。 - 直接内存(NIO DirectBuffer) → 进程级共享 (堆外)→
OutOfMemoryError: Direct buffer memory
;ByteBuffer.allocateDirect()
/mmap 用于少拷贝 I/O。
2 栈错误对比:StackOverflowError
vs 栈相关 OutOfMemoryError
-
触发
SOE
:深递归/极大方法嵌套导致栈深超限 (受-Xss
影响)。OOME(栈)
:创建线程失败 (OS 线程/内存/进程限制)或单线程-Xss
过大导致总内存不足 (常见报文unable to create native thread
)。
-
复现
SOE
:小栈-Xss256k
+ 递归方法死递归;OOME(栈)
:-Xss8m
+ 循环创建休眠线程直到失败。
-
排查
- 看异常栈&
-Xss
; jcmd <pid> Thread.print
看线程数/状态,核对 OS/容器限制(ulimit -u
)。
- 看异常栈&
3 四类 OOM 的触发条件与首条线索
- Java heap space :堆无法分配新对象(泄漏/短时分配暴涨/堆太小)→ 首看 GC 日志 与 heap dump。
- GC overhead limit exceeded :大部分时间在 GC,回收极少 (近似 98%/2%)→ 首看 GC 日志确认症状,根因仍指向堆紧张。
- Metaspace :类元数据空间耗尽(动态生类/类加载器泄漏)→ 先看 NMT 与 class loader stats。
- Direct buffer memory :直接内存超上限或未释放 → 查
-XX:MaxDirectMemorySize
、allocateDirect
使用点与 NMT 的NIO
类别。
4 如何可靠复现 Metaspace OOM(思路)
- 做法 :用 ByteBuddy/CGLIB/ASM 不断生成新类 ;每批使用新 ClassLoader 并把 Loader 放入
static List
强引用防卸载;运行时加-XX:MaxMetaspaceSize=64m
。 - 第一条排查线索 :
OOME: Metaspace
报错出现后,立刻jcmd <pid> VM.native_memory summary
与jcmd <pid> GC.class_stats / VM.class_loader_stats
。 - 防护 :① 复用 Loader/卸载前清所有强引用(含 TCCL/缓存/ThreadLocal);② 设上限并监控 NMT 指标。
5 运行时常量池 vs 字符串常量池(StringTable)
-
位置/时机
- 运行时常量池 :在方法区(JDK8+ 为 Metaspace);随类加载/链接建立。
- 字符串常量池 :JDK7+ 在堆 (JDK6 在 PermGen);JVM 启动即有,运行期通过字面量/
intern()
填充。
-
典型 OOM
- 常量池:
OOME: Metaspace
(或常量池相关文案)。 - 字符串池(堆):
OOME: Java heap space
(或 GC overhead)。
- 常量池:
-
示例(施压 StringTable)
javawhile (true) UUID.randomUUID().toString().intern();
6 new
对象的关键流程(6 步)
- 类可用性检查 :未加载/未初始化则触发 加载→链接(验证/准备/解析)→初始化。
- 分配 :优先在 TLAB (线程本地)用指针碰撞 ;不够时到共享堆用 CAS/加锁。
- 清零:实例字段所在内存全部置 0(得默认值)。
- 对象头 :写入 Mark Word (哈希/锁标志/偏向/年龄)与 Klass 指针 (数组还写长度)。
- 执行
<init>
:按继承层级构造;引用写入触发写屏障维护卡表。 - 失败分支 :分配失败→尝试 GC/扩堆;仍失败→
OOME: Java heap space
。
7 新生代/晋升/G1&ZGC
- Eden / S0 / S1 :Young GC 时将 Eden+from 的幸存者复制到 to ,对象年龄+1 ,然后 from/to 互换。
- 三条晋升路径 :① 到达 MaxTenuringThreshold ;② 动态年龄判定 (某年龄及以上占 Survivor 超阈值→直晋升);③ 大对象 直接进老年代/巨型区(Parallel 的
PretenureSizeThreshold
;G1 Humongous ≥ 半个 Region)。 - G1 :Region 化 + 分代(Young/Mixed GC),复制晋升;Humongous 用一组连续 Region(通常计入 Old)。
- ZGC :早期不分代 ,用染色指针+读屏障 并发重定位;JDK 21+ 可开启
-XX:+ZGenerational
引入分代,仍保持低停顿。
8 OOM ↔ 关键参数/限制配对
- Java heap space →
-Xmx/-Xms
(堆不足);先看 GC 日志/heap dump。 - GC overhead limit exceeded → 根因仍是
-Xmx
紧张(可临时-XX:-UseGCOverheadLimit
获取证据)。 - Metaspace →
-XX:MaxMetaspaceSize
(类元空间不足);看 NMT/类加载器。 - Direct buffer memory →
-XX:MaxDirectMemorySize
(直接内存上限);看 NMTNIO
。 - unable to create native thread → OS/容器线程&内存限制(亦受
-Xss
影响)。
9 Heap OOM 的最小闭环(三步)
- 留证 :
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/heap.hprof -Xlog:gc*:file=/var/gc.log
; 必要时jcmd <pid> GC.heap_dump /tmp/heap.hprof
。 - 定位 :MAT 看 Leak Suspects / Dominator Tree / Path to GC Roots;结合 GC 日志判断"泄漏"vs"分配暴涨"。
- 处置 :短期限流/减并发/小幅调 Xmx 、收紧缓存;长期修引用链/类加载器/缓存策略 并加 JFR/GC/NMT 监控。
10(可选延伸)Safepoint 与"进入慢"
- 定义 :在 GC/类卸载/去优化等全局操作前,JVM 要求所有 Java 线程停在带轮询的安全点 ,以便一致地枚举根并扫描。
- 进入慢的常见原因 :① 紧凑长循环/大方法 缺少计数 Safepoint;② JNI Critical/长时间阻塞 I/O导致线程久不回到轮询点。
- 对策 :开
-Xlog:safepoint
或-XX:+PrintSafepointStatistics
定位;拆循环/开启-XX:+UseCountedLoopSafepoints
、缩短 JNI 临界区/改为可中断阻塞。
11 String
不可变性的利与弊 & 高拼接实践
- 利 :可放入常量池、可缓存
hashCode
、天然线程安全,适合做 Map Key/跨线程共享。 - 弊 :拼接/替换会生成新对象,循环中易分配放大→ GC 压力。
- 实践 :循环中用
StringBuilder
(预估容量) ;批量拼接用StringJoiner/Collectors.joining
;流式写入StringWriter/BufferedWriter
;慎用intern()
处理动态值。
12 对象内存布局 & 访问方式
- 布局 :对象头 (Mark Word + Klass 指针;数组还有长度 )+ 实例数据 (父类→子类,遵循对齐)+ 对齐填充 (HotSpot 默认8 字节对齐)。
- Mark Word 含义 :哈希、锁状态/偏向信息、GC 年龄等;64 位下开 Compressed Oops/Class Pointers 以32 位编码引用,降低内存占用/提高缓存友好。
- 访问方式 :句柄 (引用→句柄表项→{对象地址, 类型元数据地址},移动对象只改句柄,多一次间接 );直接指针 (HotSpot 采用,更快,移动需更新引用)。
13 ThreadLocal 为何"看起来泄漏" & 防范
- 原因 :
ThreadLocalMap
的 key(ThreadLocal)是弱引用 、value 是强引用 ;当 key 被 GC 回收后,value 仍被线程强引用着,线程池中线程长寿导致 value 长期不清。 - 防范 :①
try{ set } finally{ remove(); }
;② 线程池统一清理(装饰afterExecute
)并避免放大对象 ;必要时禁用/谨慎用InheritableThreadLocal
。
14 用 NMT 排查 Metaspace/Direct Memory
-
启动 :
-XX:NativeMemoryTracking=summary
(或detail
)可选-XX:+PrintNMTStatistics
。 -
在线:
bashjcmd <pid> VM.native_memory baseline jcmd <pid> VM.native_memory summary.scale=MB jcmd <pid> VM.native_memory detail.diff
关注:Class(=Metaspace) 、NIO(=Direct)、Thread/Code/Internal/Symbol。
-
第一动作 :Metaspace 涨→看 class_loader_stats /动态生类点;Direct 涨→核对 MaxDirectMemorySize ,定位
allocateDirect
持有链(Netty 可开 leakDetector)。
15 直接内存 vs 堆(实务四句)
- 管理者 :堆→GC ;直接内存→
DirectByteBuffer
的 Cleaner/Unsafe 释放,最终由 OS 回收。 - 场景 :堆→业务对象/集合;直接内存→NIO/Netty/mmap 减少拷贝。
- 参数 :堆→
-Xms/-Xmx
;直接内存→-XX:MaxDirectMemorySize
。 - 首证据 :堆→
OOME: Java heap space
+ GC 日志/heap dump;直接内存→OOME: Direct buffer memory
+ NMTNIO
。
16 PermGen → Metaspace(JDK8 迁移)
- 差异 :**PermGen(JDK8 前,堆内)**由
-XX:PermSize/MaxPermSize
控制;**Metaspace(JDK8+ 本地内存)**默认随系统增长,用-XX:MaxMetaspaceSize
控制。 - 典型 OOM :
OOME: PermGen space
(旧) /OOME: Metaspace
(新)。 - 遇到 Metaspace OOM 首动 :看 NMT 与 class loader stats,确认是否类加载器泄漏/动态生类过多。
17 任意 OOM 的"最小证据包"(5 条)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/heap.hprof
------ OOM 自动落地 dump-Xlog:gc*:file=/var/gc.log:time,uptime,tags
------ 持久化 GC 证据jcmd <pid> GC.heap_dump /tmp/heap.hprof
------ 在线抓堆jcmd <pid> VM.native_memory summary.scale=MB
------ 看本地内存分类jcmd <pid> Thread.print
------ 线程快照(线程 OOME/卡死)
18 用 MAT 看 dump 的三步法
- Leak Suspects:看是否存在可疑泄漏链/最大保留大小持有者。
- Dominator Tree / Top Consumers :按Retained Size 找"内存大户"(集合/缓存/字节数组)。
- Histogram + Path to GC Roots :锁定异常类型后,沿强引用链/静态字段/ThreadLocal 找根因。
19 TLAB(线程本地分配缓冲)
- 作用/好处 :线程私有指针碰撞快速分配,无锁/无 CAS;提升缓存局部性、降低竞争。
- 代价/局限 :内部碎片(零头浪费)、申请/回收 TLAB 的额外开销。
- 参数/观察 :
-XX:+/-UseTLAB
;-Xlog:gc+tlab=info
;-XX:+ResizeTLAB
、-XX:TLABSize
。 - 回退场景 :对象太大 或当前 TLAB 剩余不足→到共享 Eden 分配/申请新 TLAB;G1 的巨型对象可能绕过新生代。
20 方法区/常量池/字符串池的位置变迁(JDK6 → 7 → 8)
- JDK6 :方法区=PermGen(堆内) ;运行时常量池/字符串常量池 在 PermGen;报错
OOME: PermGen space
;调参-XX:MaxPermSize
。 - JDK7 :仍有 PermGen,但字符串常量池迁至堆 ;堆压力增大可能导致 heap OOM。
- JDK8 :移除 PermGen,用 Metaspace(本地内存) ;运行时常量池 随类元数据在 Metaspace,字符串常量池在堆 ;报错
OOME: Metaspace
;调参-XX:MaxMetaspaceSize
。
21 遇到 OOM 的标准操作流程(6 步)
- 启动就留证 :开启 HeapDumpOnOOME 与 GC 日志。
- 线上取证 :
jcmd ... GC.heap_dump
/VM.native_memory
/Thread.print
,必要时开 JFR 5--10 分钟。 - 快速研判 :结合 OOME 文案/NMT 类别判断 heap / metaspace / direct / threads。
- 离线定位 :MAT 看 Leak Suspects → Dominator Tree → Path to GC Roots ;若是 metaspace/direct,看 class loader/NIO。
- 短期止血 :限流降并发/缩小批量;谨慎小幅 调
-Xmx
;收紧缓存;保留证据后重启。 - 长期治理 :修复引用链/类加载器/缓存策略 ;加 JFR/GC/NMT 周期快照与告警。