JVM 内存结构
面向运维+面试的 JVM 运行时数据区详解,涵盖五大区域作用、配置参数、溢出异常、排查要点。 面向运维+面试,区分线程私有/线程共享、区域作用、溢出异常、运维排查要点,不深挖源码,侧重实际现象与问题定位。
一、整体总览
Java 程序运行时,JVM 会划分出5大运行时数据区,依据「是否随线程创建/销毁」分为两大类:
- 线程私有区域:每个线程独有,线程创建时分配,线程销毁时回收,线程间互不干扰
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
- 线程共享区域:整个 JVM 实例共用,随 JVM 启动创建、关闭销毁
- Java 堆
- 方法区(JDK8 后改为元空间)
补充:直接内存(堆外内存)不属于 JVM 运行时数据区,但运维高频接触,单独讲解。
二、线程私有区域
1. 程序计数器(Program Counter Register)
1.1 作用
- 记录当前线程正在执行的字节码行号。
- Java 是多线程,CPU 切换线程执行时,靠此区域记录中断位置,线程恢复后继续执行。
- 如果执行 Native 本地方法,计数器值为 undefined。
1.2 核心特点
- 唯一不会抛出
OutOfMemoryError(OOM)的区域:内存占用极小、固定大小。 - 生命周期:跟随线程,线程销毁即释放。
运维关注点
日常故障几乎不会涉及,了解概念即可。
2. Java 虚拟机栈(Java Virtual Machine Stack)
2.1 核心结构
每个线程对应一个虚拟机栈,栈中存储栈帧(Stack Frame)。
每调用一个 Java 方法,就创建一个栈帧;方法执行完毕,栈帧出栈销毁。
单个栈帧包含:
- 局部变量表:存放方法内局部变量、方法参数(基本类型、对象引用地址)
- 操作数栈:字节码执行时的临时数据栈,做计算、传参
- 动态链接、方法返回地址、附加信息:底层逻辑,运维无需深究
2.2 内存配置
启动参数:-Xss,设置单个线程栈的大小。
例:-Xss256k 表示每个线程栈容量为 256KB。
2.3 两类异常(运维高频)
- StackOverflowError 栈溢出
-
触发原因:
-
方法无限递归(最典型);
-
单个线程栈
-Xss设置过小; -
方法调用层级过深。
-
现象:程序直接抛出栈溢出错误,线程挂掉。
- OutOfMemoryError: Unable to create new native thread
- 触发原因:
- 单个线程栈
-Xss太大,有限内存下,能创建的线程总数变少; - 应用线程数持续暴涨(连接不释放、线程泄漏);
- 操作系统用户进程/线程数上限被打满。
- 场景:服务假死、无法接收新请求、创建线程失败。
运维排查
- 栈溢出:
jstack查看线程栈,定位无限递归/深层调用的方法; - 无法创建线程:先查线程总数、
-Xss配置,再查系统线程限制。
3. 本地方法栈(Native Method Stack)
3.1 作用
功能和Java 虚拟机栈完全一致,区别只在于:
- 虚拟机栈:执行 Java 代码;
- 本地方法栈:执行 Native 本地方法(由 C/C++ 实现,如
System.currentTimeMillis()、文件、网络、系统调用)。
3.2 异常与配置
- 同样会抛出
StackOverflowError和 OOM; - JVM 一般不单独提供参数配置,大小跟随虚拟机栈。
运维关注点
极少单独出问题,和虚拟机栈问题统一排查即可。
三、线程共享区域(运维重点,80% 内存故障出自这里)
1. Java 堆(Heap)
1.1 作用
JVM 最大的内存区域,所有对象实例、数组都在堆中分配内存。
是 GC(垃圾回收)的主要工作区域。
1.2 内存配置(生产必配参数)
-Xms :堆初始内存
-Xmx :堆最大内存
✅ 生产规范:-Xms = -Xmx,避免运行时堆动态扩容/缩容带来性能抖动。
1.3 堆分代模型(GC 核心,必学)
现代 JVM 把堆划分为 新生代 + 老年代,分代目的:针对不同生命周期对象,使用不同回收算法,提升 GC 效率。
(1)新生代(Young Generation)
存放生命周期短、朝生夕灭的对象(临时对象、局部对象),GC 频率极高。
内部再划分 3 块:
- Eden(伊甸区):新对象优先分配在这里,占新生代 8/10
- Survivor 幸存区:分为
From(S0)/To(S1),各占 1/10
规则:同一时间永远只有一块 Survivor 在用,另一块为空。
新生代比例参数:
-XX:SurvivorRatio=8 → Eden:S0:S1 = 8:1:1(默认值)
(2)老年代(Old Generation / Tenured)
存放存活时间久、体积大的对象:
- 新生代多次 GC 后依然存活的对象;
- 超过阈值的大对象(直接进入老年代,避免新生代复制开销);
- 动态晋升的对象。
GC 频率低、回收速度慢,一旦触发 Full GC 会造成明显业务卡顿。
对象流转完整流程
- 新对象 → Eden 区;
- Eden 区满 → 触发 Minor GC(新生代GC);
- 存活对象移入空闲的 Survivor 区,对象年龄 +1;
- 对象年龄达到阈值(默认15)→ 晋升老年代;
- 老年代空间不足 → 触发 Major GC / Full GC。
1.4 堆内存溢出:java.lang.OutOfMemoryError: Java heap space
触发场景
- 内存泄漏:对象被长生命周期引用持有,无法被 GC 回收,内存缓慢上涨,最终占满堆;
- 内存溢出(非泄漏):瞬时并发、批量数据查询,创建大量临时对象,堆容量不足;
-Xmx设置过小,无法承载业务峰值。
运维排查手段
jstat -gc观察 GC 频率、堆使用率;- 开启 GC 日志,分析回收效果;
jmap生成堆快照,用 MAT 分析大对象、泄漏对象。
2. 方法区(Method Area)& 元空间(Metaspace)
2.1 演进历史(关键考点)
- JDK 1.6 及之前:永久代(PermGen),属于堆的一部分,受
-Xmx限制; - JDK 1.7:逐步剥离,字符串常量池、静态变量移出永久代;
- JDK 8 及以后:彻底移除永久代,改用元空间 Metaspace。
元空间直接使用宿主机本地物理内存,不再占用堆内存。
2.2 存储内容(JDK8 元空间)
存储类的元数据(描述类本身的信息):
- 加载的类、接口、注解、枚举信息;
- 方法信息、字段信息、运行时常量池;
- 动态生成的类(CGLIB 动态代理、ASM、热部署、反射、Groovy 等)。
2.3 核心配置参数(JDK8+)
-XX:MetaspaceSize 元空间初始大小
-XX:MaxMetaspaceSize 元空间最大上限(建议手动设置,防止无限占用系统内存)
2.4 元空间溢出:java.lang.OutOfMemoryError: Metaspace
典型触发场景(运维高频)
- 应用热部署(反复加载/卸载类);
- 大量使用动态代理、反射、字节码增强;
- 框架、中间件批量动态生成类;
MaxMetaspaceSize设置过小。
现象与排查
- 现象:服务启动失败、运行中突然崩溃,日志报 Metaspace OOM;
- 排查:查看已加载类数量、动态类生成逻辑,调整元空间上限。
四、补充:直接内存(堆外内存 Direct Memory)
不属于 JVM 运行时数据区,但运维、中间件(Netty、NIO)必接触。
4.1 作用
- NIO、Netty 等框架使用 DirectByteBuffer 分配堆外内存;
- 读写文件、网络数据时,避免堆内存与系统内存拷贝,提升IO性能。
4.2 特点
- 不受
-Xmx堆大小限制,使用操作系统物理内存; - 回收依赖系统 GC 或手动释放,JVM GC 无法直接管理;
4.3 溢出异常
OutOfMemoryError: Direct buffer memory
触发原因
- 频繁创建堆外缓冲区但不主动释放;
- 堆外内存使用无限制,占满系统内存;
- Netty/NIO 组件使用不当、连接泄漏。
4.4 配置参数
-XX:MaxDirectMemorySize:限制最大堆外内存。
五、区域汇总对比表(背诵版)
| 内存区域 | 归属 | 主要存储内容 | 常见异常 | 核心配置参数 |
| 程序计数器 | 线程私有 | 字节码执行行号 | 无OOM | 无 |
| Java 虚拟机栈 | 线程私有 | 栈帧、局部变量、方法调用 | StackOverflow、无法创建线程 | -Xss |
| 本地方法栈 | 线程私有 | Native 方法栈帧 | 同虚拟机栈 | 无单独参数 |
| Java 堆 | 线程共享 | 对象、数组、分代内存 | Java heap space OOM | -Xms、-Xmx、-XX:SurvivorRatio |
| 元空间(JDK8+) | 线程共享 | 类元数据、常量池 | Metaspace OOM | -XX:MetaspaceSize、MaxMetaspaceSize |
| 直接内存 | 堆外 | NIO/Netty 缓冲区 | Direct buffer OOM | -XX:MaxDirectMemorySize |
六、运维核心总结 & 排查思路
- 栈相关问题:优先看
jstack,定位递归、线程泄漏、线程数打满; - 堆相关问题:线上第一看
jstat+ GC 日志,OOM 后用jmap+MAT 分析堆快照,区分「内存泄漏」和「容量不足」; - 元空间问题:重点排查热部署、动态代理、字节码框架,合理设置元空间上限;
- 堆外内存:Netty、NIO 应用专属,检查连接/缓冲区是否泄漏,限制最大堆外内存;
- 容器环境额外注意:Docker 中 JVM 要开启 CGroup 感知,避免 JVM 无视容器内存限制被系统 OOM Kill。
七、面试高频问答(运维岗)
- JVM 运行时数据区分为哪几块?哪些线程私有、哪些共享?
答:线程私有:程序计数器、虚拟机栈、本地方法栈;线程共享:堆、元空间。程序计数器是唯一无OOM的区域。 - 虚拟机栈和堆的区别?
答:栈存方法调用、局部变量,线程私有,生命周期短;堆存对象实例,线程共享,GC 主要区域,容易发生内存溢出。 - JDK8 为什么移除永久代改用元空间?
答:永久代大小难预估、易OOM;元空间使用系统物理内存,弹性更好,同时简化 JVM 实现。 - 堆内存溢出和元空间溢出分别是什么原因?
答:堆溢出多为内存泄漏或堆大小不足;元空间溢出多为热部署、动态生成类、元空间配置过小。
关联文档
- JVM 参数优化 — JVM 内存参数配置
- JVM 学习路线与故障排查 — JVM 学习路线与线上故障处理
- ../../../../english/vocabulary/云原生术语 — 相关技术术语