用途:判断“内存去哪了”、哪块区域会 OOM 时查阅
| 区域 |
线程共享 |
存什么 |
溢出表现 |
| 堆 |
共享 |
对象实例、数组 |
java.lang.OutOfMemoryError: Java heap space |
| 元空间(Metaspace) |
共享 |
类元数据、方法字节码结构、运行时常量池 |
Metaspace(类加载器泄漏、动态代理生成类) |
| 栈 |
每线程 |
栈帧:局部变量表、操作数栈、动态链接 |
StackOverflowError(深递归)/ unable to create new native thread |
| 程序计数器 |
每线程 |
当前字节码行号 |
几乎不排查 |
| 本地方法栈 |
每线程 |
JNI 调用 |
少见 |
| 直接内存(堆外) |
共享 |
NIO DirectBuffer、Netty 池、MMAP |
Direct buffer memory OOM,堆 dump 里看不到 |
- 字符串常量池在堆中(JDK 7 起从永久代移来),
intern() 行为随之改变。
- 对象头包含 Mark Word(哈希、GC 年龄、锁状态)与类指针;开启压缩指针时 64 位对象头通常 12 字节,加对齐填充。小对象大量存在时,头部与对齐开销可能超过字段本身。
- 逃逸分析决定标量替换与栈上分配是否发生:未逃逸对象可能不真正分配在堆上,但不要把“逃逸分析”当调优开关依赖,它受 JIT 判断影响。
- 堆外内存不受 GC 直接管理,靠
Cleaner/显式释放;-XX:MaxDirectMemorySize 默认约等于堆大小。堆外泄漏表现为 RSS 持续高于 -Xmx。
RSS ≈ 堆 committed + 元空间 + 线程栈(线程数 × -Xss) + 代码缓存 + GC/JIT 结构 + 堆外直接内存 + glibc arena
常见“堆没满但被容器 OOMKill”的原因:线程数过多(每线程默认 1MB 栈)、堆外未限、glibc malloc arena 碎片。对策:限制线程数与 -Xss、显式设置 MaxDirectMemorySize、必要时用 malloc arena 调优或换分配器。
| 目的 |
命令/工具 |
| 堆使用与分代 |
jstat -gcutil <pid> 1s、JMX、Prometheus JVM exporter |
| 对象分布 |
jmap -histo:live、MAT 分析 dump、Arthas heapdump + 分析 |
| 类加载数量 |
jstat -class、-XX:+TraceClassLoading、Arthas sc/classloader |
| 线程与栈 |
jstack、Arthas thread |
| 堆外 |
Native Memory Tracking(-XX:NativeMemoryTracking=summary + jcmd VM.native_memory) |
| 误区 |
修正 |
容器里 -Xmx 等于容器内存 |
必须为栈、元空间、堆外留余量(经验值 25-40%) |
| RSS 高就是内存泄漏 |
先看 NMT 拆解,堆外与线程栈是常见来源 |
| 永久代说法沿用 |
JDK 8 起为元空间,参数与行为不同 |
| 只看堆 dump 判断全部内存 |
堆 dump 不含栈与堆外 |
- 《深入理解 Java 虚拟机》(周志明)第 2、3 章
- OpenJDK HotSpot GC Tuning 文档(内存区域与参数)
- Tim Boudreau / Nitsan Wakart 关于堆外内存与 RSS 的公开文章