内存模型与内存区域

核心概念

JVM 运行时数据区是 Java 程序运行时内存的抽象模型,理解各区域的作用和特点是学习 JVM 的基础。

运行时数据区

1. 程序计数器(Program Counter Register)

  • 线程私有

  • 记录当前线程执行的字节码指令地址

  • 如果执行 Native 方法,计数器值为 Undefined

  • 唯一不会发生 OOM 的区域

2. Java 虚拟机栈(JVM Stack)

  • 线程私有,生命周期与线程相同

  • 每个方法执行时创建一个栈帧(Stack Frame)

栈帧结构

┌─────────────────────────┐
│        栈帧              │
├─────────────────────────┤
│  局部变量表              │ ← 存放方法参数和局部变量
│  (Local Variable Table) │    基本类型直接存值
│                         │    引用类型存指针
├─────────────────────────┤
│  操作数栈                │ ← 字节码指令的操作空间
│  (Operand Stack)        │    类似 CPU 的寄存器
├─────────────────────────┤
│  动态链接                │ ← 指向运行时常量池的
│  (Dynamic Linking)      │    方法引用
├─────────────────────────┤
│  方法返回地址             │ ← 方法正常/异常退出
│  (Return Address)       │    返回的位置
└─────────────────────────┘

异常情况

  • StackOverflowError:栈深度超过限制(递归调用过深)

  • OutOfMemoryError:无法申请到足够内存(动态扩展时)

3. 本地方法栈(Native Method Stack)

  • 线程私有

  • 为 Native 方法服务

  • HotSpot 将虚拟机栈和本地方法栈合二为一

4. Java 堆(Heap)⭐⭐⭐⭐⭐

  • 线程共享,JVM 启动时创建

  • GC 的主要管理区域

  • 几乎所有对象实例都在这里分配

堆内存结构(分代)

JVM 参数

堆内存 OOM

5. 方法区(Method Area)

  • 线程共享

  • 存储类信息、常量、静态变量、JIT 编译后的代码

实现演变

JDK 版本
实现方式
存储位置

JDK 7 及之前

永久代(PermGen)

JVM 内存

JDK 8 及之后

元空间(Metaspace)

本地内存(Native Memory)

为什么用元空间替换永久代

  1. 永久代大小难以确定:容易出现 OOM

  2. GC 效率低:永久代的回收条件苛刻

  3. 与 JRockit 合并:JRockit 没有永久代的概念

  4. 灵活性更高:元空间使用本地内存,不受 JVM 堆大小限制

元空间参数

6. 运行时常量池(Runtime Constant Pool)

  • 方法区的一部分

  • Class 文件中的常量池表在类加载后进入运行时常量池

  • 具有动态性:运行时也可以添加常量(如 String.intern()

7. 直接内存(Direct Memory)

  • 不属于 JVM 运行时数据区

  • NIO 使用 DirectByteBuffer 直接操作堆外内存

  • 通过 -XX:MaxDirectMemorySize 控制大小

  • 避免在 Java 堆和 Native 堆之间复制数据

对象的创建过程

对象的内存布局

对象头(Object Header)

在 64 位 JVM 中:

实例数据(Instance Data)

  • 对象真正存储的有效信息

  • 字段的存储顺序受分配策略和定义顺序影响

  • 相同宽度的字段总是被分配在一起

对齐填充(Padding)

  • HotSpot 要求对象起始地址必须是 8 字节的整数倍

  • 不够则填充

对象的访问定位

句柄访问

  • 优点:对象移动时只需修改句柄中的指针

  • 缺点:两次指针定位

直接指针(HotSpot 使用)

  • 优点:速度快,一次指针定位

  • 缺点:对象移动时需要修改栈中引用

面试要点

高频问题

  1. JVM 运行时数据区有哪些?各自的作用?

    • 程序计数器、虚拟机栈、本地方法栈(线程私有)

    • 堆、方法区(线程共享)

  2. 堆和栈的区别?

    • 栈:线程私有,存储局部变量和方法调用,自动分配和回收

    • 堆:线程共享,存储对象实例,由 GC 管理

  3. 什么是 TLAB?为什么需要它?

    • Thread Local Allocation Buffer,线程本地分配缓冲区

    • 每个线程在堆的 Eden 区预先分配一小块内存

    • 避免多线程分配内存时的锁竞争

  4. 永久代和元空间的区别?

    • 永久代使用 JVM 内存,元空间使用本地内存

    • 元空间默认无上限,更不容易 OOM

    • JDK 8 起永久代被移除

  5. String.intern() 在 JDK 6 和 JDK 7+ 的区别?

    • JDK 6:将字符串复制到永久代的字符串常量池

    • JDK 7+:字符串常量池移到堆中,intern() 只存引用

常见陷阱

  • 不是所有对象都在堆上分配(逃逸分析 → 栈上分配)

  • 方法区不等于永久代,永久代只是 HotSpot 对方法区的实现

  • -Xss 设置的是每个线程的栈大小,不是所有线程共享

参考资料

Last updated