JVM 遇到的问题-1.0
Q1. java 虚拟机规范定义的运行时数据区有几块?分别是什么?
A :一共有5块
B分别是:程序计数器,java 堆,java虚拟机栈,本地方法栈,方法区(jdk8之后改成元空间)。
⚠️ 注意事项:
1). 直接内存不属于运行时数据区;
2). jdk7之前方法区实现为永久代PermGen,jdk8之后彻底移除永久代,使用元空间。
3). 程序计时器是唯一没有OOM的内存区域。
Q2.直接内存是什么?又叫什么?
A: 直接内存又称为堆外内存。通过Unsafe向操作系统直接申请的内存,
典型 API:ByteBuffer.allocateDirect(),Netty 大量使用堆外缓冲区。存放 NIO 网络 IO 的数据缓冲区;不在 Java 堆中,不受-Xmx/-Xms限制。
⚠️ 注意事项:
- 堆上只会生成一个很小的DirectByteBuffer代理对象,真实数据存在堆外;
- 依靠Cleaner虚引用异步回收,极易发生堆外内存泄漏;
- 调控参数:-XX:MaxDirectMemorySize;OOM 报错:Direct buffer memory。
Q3:JVM 的组成 = 运行时数据区 + 直接内存?
A:这样说,表述不严谨,是不对的。JVM 完整的架构是五大子系统:
类加载子系统,运行时数据区,执行引擎,gc子系统,本地方法接口。
B: 从进程内存占用视角看,JVM 进程使用的内存包含:运行时数据区 + 直接内存。直接内存不属于 JVM 规范定义的运行时数据区,只是排查内存故障时需要一并分析。
Q4:如何系统掌握各区域 OOM 场景,并且动手复现?
1)熟记映射表:异常日志 → 定位内存区域
|------------------------------------|-----------|
| 报错信息 | 对应区域 |
| Java heap space | Java 堆 |
| Metaspace | 元空间 (方法区) |
| Direct buffer memory | 直接内存 |
| unable to create new native thread | 虚拟机栈 |
| StackOverflowError | 虚拟机栈 |
2)每一个 OOM Demo 强制配套 3 组 JVM 参数
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text # 1. GC日志输出 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log # 2. OOM自动dump堆快照 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=oom.hprof # 3. 限制对应区域内存,稳定复现 |
3)分层练习
① 基础 Demo 复现(while 循环简易案例)
② 模拟业务真实内存泄漏(ThreadLocal、静态缓存、动态类)
③ 使用 MAT 分析 hprof 快照,定位引用链,区分【内存泄漏 / 单纯内存不足】
Q5:OOM 发生之后,JVM 进程一定会退出吗?
A:不一定。
如果代码捕获 OOM 异常,当前线程终止,其他线程可继续运行;持续分配对象会反复抛出 OOM,进程不会直接死掉。
Q6:StackOverflowError 和 OOM 的本质区别?
A:
- StackOverflowError:单个线程栈空间容量不足,方法调用层级过深;
- unable to create new native thread OOM:系统资源不足,无法新建线程分配栈内存。
Q7:发生堆 OOM,一定是堆太小吗?
A:不一定。两种场景:
- 业务流量变大,对象总量上升 → 需要调大堆;
- 代码内存泄漏,无用对象持续占用 → 改代码,单纯调堆治标不治本。
Q8:为什么 Netty 优先使用直接内存?
A:减少一次内核缓冲区与 JVM 堆缓冲区的数据拷贝(减少一次内存复制,零拷贝思想,提升 IO 性能);代价是堆外内存管理复杂,容易泄漏。
Q9:程序计数器为什么不会产生 OOM?
A:每条线程的程序计数器只保存一条指令地址,内存占用极小;JVM 规范明确规定此区域不需要回收,不会出现内存溢出。