Skip to content

JVM 运行时数据区和 OOM 排查

理解 JVM 运行时数据区时,要把内存区域、GC、OOM 和线上排查放在一起看。只有知道每个内存区域负责什么,才能判断不同异常对应的根因。


1. JVM 运行时数据区

常见运行时数据区:

text
线程私有:
  - 程序计数器
  - Java 虚拟机栈
  - 本地方法栈

线程共享:
  - Java 堆
  - 方法区 / 元空间
  - 运行时常量池
  - 直接内存

2. 程序计数器

程序计数器记录当前线程正在执行的字节码行号。

特点:

  • 线程私有。
  • 占用内存很小。
  • 是 JVM 规范中唯一没有规定 OutOfMemoryError 的区域。

3. Java 虚拟机栈

每个线程都有自己的虚拟机栈。每次方法调用都会创建一个栈帧。

栈帧中包含:

  • 局部变量表。
  • 操作数栈。
  • 动态链接。
  • 方法返回地址。

常见异常:

异常原因
StackOverflowError栈深度过深,例如无限递归
OutOfMemoryError: unable to create new native thread线程太多,无法创建新线程

4. Java 堆

堆是对象实例主要分配区域,也是 GC 管理的核心区域。

常见异常:

text
java.lang.OutOfMemoryError: Java heap space

常见原因:

  • 对象创建速度太快。
  • 大对象占用内存。
  • 集合持续增长。
  • 缓存没有淘汰。
  • 内存泄漏导致对象无法回收。

5. 方法区 / 元空间

JDK 8 之后,方法区的实现从永久代变成元空间,元空间使用本地内存。

主要存放:

  • 类元信息。
  • 方法信息。
  • 字段信息。
  • 常量池相关信息。
  • JIT 编译后的代码缓存。

常见异常:

text
java.lang.OutOfMemoryError: Metaspace

常见原因:

  • 动态生成大量类。
  • CGLIB、动态代理使用不当。
  • 类加载器泄漏。
  • 热部署频繁导致旧类无法卸载。

6. 直接内存

直接内存不属于 Java 堆,但会占用进程内存。

常见使用方:

  • NIO DirectByteBuffer
  • Netty。
  • 文件传输和零拷贝。

常见异常:

text
java.lang.OutOfMemoryError: Direct buffer memory

排查时不能只看堆,还要看进程 RSS、直接内存和本地内存。


7. 常见 OOM 类型

异常典型原因
Java heap space堆对象过多或泄漏
GC overhead limit exceededGC 花费大量时间但回收很少
Metaspace类元信息过多或类加载器泄漏
Direct buffer memory直接内存不足
unable to create new native thread线程数过多或系统资源不足
Requested array size exceeds VM limit数组过大

8. OOM 排查流程

建议流程:

text
确认异常类型
  -> 保留现场日志和 dump
  -> 看 JVM 参数和内存限制
  -> 用 jstat 看 GC 情况
  -> 用 jmap 导出堆
  -> 用 MAT / JProfiler / VisualVM 分析大对象和引用链
  -> 定位代码中的持有方

常用参数:

bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump
-XX:+PrintGCDetails

JDK 9+ 常用统一日志:

bash
-Xlog:gc*:file=gc.log:time,uptime,level,tags

9. CPU 飙高排查

流程:

text
top 找到 Java 进程
top -Hp <pid> 找到高 CPU 线程
printf "%x\n" <tid> 转成十六进制
jstack <pid> 找到对应 nid
分析线程栈

常见原因:

  • 死循环。
  • 正则回溯。
  • 频繁 GC。
  • 锁竞争严重。
  • 线程池任务堆积。

10. 频繁 Full GC 排查

关注点:

  • 老年代是否持续增长。
  • Full GC 后内存是否明显下降。
  • 是否有大对象直接进入老年代。
  • 是否存在内存泄漏。
  • 元空间是否持续增长。

如果 Full GC 后内存下降明显,可能是分配压力大或参数不合理。

如果 Full GC 后内存下降很少,通常要怀疑内存泄漏或长期存活对象过多。


11. 理解检查

堆和栈的区别?

堆用于存放对象实例,线程共享,由 GC 管理;栈用于方法调用,线程私有,方法结束后栈帧出栈。

元空间为什么还会 OOM?

元空间使用本地内存,不代表无限。动态类太多、类加载器泄漏、元空间上限太小都会导致 OOM。

jmapjstack 分别看什么?

jmap 主要看堆对象和 dump;jstack 看线程栈、死锁、阻塞和 CPU 飙高线程。


12. 总结

OOM 排查不能只记命令。先判断异常对应的内存区域,再结合 GC 日志、线程栈、堆 dump 和 JVM 参数定位。真正要掌握的是“区域 -> 现象 -> 工具 -> 根因”的链路。

Released under the MIT License.