JVM 内存模型详解
JVM(Java Virtual Machine)是 Java 程序运行的基石,理解它的内存模型是排查 OOM、调优 GC 的前提。
一、运行时数据区
JVM 在运行程序时会把内存划分为以下几个区域:
┌─────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├──────────────┬──────────────────────────────┤
│ 线程私有 │ 线程共享 │
├──────────────┼──────────────────────────────┤
│ 程序计数器 │ │
│ 虚拟机栈 │ 堆 (Heap) │
│ 本地方法栈 │ 方法区 (元空间) │
└──────────────┴──────────────────────────────┘
1. 程序计数器(PC Register)
一块较小的内存空间,记录当前线程正在执行的字节码行号。
- 线程私有
- 唯一不会发生 OOM 的区域
2. 虚拟机栈(VM Stack)
描述 Java 方法执行的内存模型。每个方法执行时都会创建一个栈帧:
public int add(int a, int b) {
int sum = a + b;
return sum;
}
栈帧包含:
- 局部变量表:存放方法参数和方法内定义的变量
- 操作数栈:存放计算过程中的中间结果
- 动态链接:指向运行时常量池的方法引用
- 方法出口:方法返回的地址
3. 本地方法栈
为 Native 方法服务的栈区,与虚拟机栈作用类似。
4. 堆(Heap)
所有线程共享,存放对象实例,是 GC 的主战场。
// new 出来的对象都在堆中
User user = new User("小宇宙", 18);
现代 JVM 将堆划分为:
- 新生代:Eden + Survivor 0 + Survivor 1(8:1:1)
- 老年代:长期存活的对象
5. 方法区(元空间)
存储类信息、常量、静态变量、JIT 编译后的代码。
JDK 8 之后,方法区的实现从永久代(PermGen)改为元空间(Metaspace),使用本地内存,避免了永久代 OOM 的问题。
二、对象创建过程
当执行 new User("小宇宙", 18) 时,JVM 内部发生的事情:
- 类加载检查:检查 User 类是否已加载、解析、初始化
- 分配内存:在堆中划出固定大小的内存
- 指针碰撞(内存规整时)
- 空闲列表(内存碎片时)
- 内存空间初始化:清零操作
- 对象头设置:设置哈希码、GC 分代年龄、锁状态
- 执行
<init>方法:调用构造函数
三、GC 工作原理
判断对象是否存活
- 引用计数法(已淘汰):循环引用问题
- 可达性分析(主流):从 GC Roots 出发是否能到达
GC Roots 包括:
- 虚拟机栈中的局部变量
- 方法区的类静态属性
- 方法区的常量
- 本地方法栈的 JNI 引用
垃圾收集算法
| 算法 | 描述 | 适用场景 |
| ---- | ---- | ---- |
| 标记-清除 | 标记存活,清除其他 | 老年代 |
| 标记-整理 | 清除后整理碎片 | 老年代 |
| 复制算法 | 两块区域来回复制 | 新生代 |
| 分代收集 | 综合以上算法 | 现代 JVM |
四、实战:OOM 排查
// 无限创建对象,触发堆 OOM
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 1MB
}
报错:java.lang.OutOfMemoryError: Java heap space
排查思路:
- 添加 JVM 参数:
-XX:+HeapDumpOnOutOfMemoryError - 使用 MAT / VisualVM 分析堆转储文件
- 定位大对象,优化代码逻辑
五、小结
| 区域 | 私有/共享 | 存储内容 | 异常 |
| ---- | ---- | ---- | ---- |
| 程序计数器 | 私有 | 字节码行号 | 不会 OOM |
| 虚拟机栈 | 私有 | 栈帧 | StackOverflowError / OOM |
| 本地方法栈 | 私有 | Native 方法 | OOM |
| 堆 | 共享 | 对象实例 | OOM |
| 方法区 | 共享 | 类信息、常量 | OOM |
小宇宙提示:掌握 JVM 内存模型,是写出高性能、高可用 Java 应用的前提。