JVM 内存模型

text
+--------------------------------------------------+
|                JVM 运行时数据区                  |
+----------------------+---------------------------+
| 线程私有             | 线程共享                  |
|----------------------|---------------------------|
| • 程序计数器         | • 堆(Heap)              |
| • 虚拟机栈           |   - 新生代(Eden, S0, S1)|
| • 本地方法栈         |   - 老年代                |
|                      | • 方法区(Metaspace)     |
+----------------------+---------------------------+

+--------------------------------------------------+
|               直接内存(Direct Memory)          |
|(不属于 JVM 规范定义,但常被使用)               |
+--------------------------------------------------+

最大、最重要的内存区域

存放对象实例、数组等。几乎所有的对象都在堆上分配(逃逸分析优化后可能栈上分配)。所有线程共享。

特点

特性说明
线程共享所有线程均可访问堆中的对象
动态分配对象在运行时动态创建,大小不固定
自动管理由 JVM 的垃圾回收器(GC)自动回收不再使用的对象
可扩展可通过 -Xms / -Xmx 设置初始和最大堆大小
物理不连续逻辑上连续,物理上可以是不连续的内存空间

堆是垃圾收集器进行GC的最重要的内存区域

内部结构

Java 堆可以分为:新生代(Eden区、S0区、S1区)和 老年代。

  • 在绝大多数情况下,对象首先分配在eden区

  • 在一次新生代GC回收后,如果对象还存活,则会进入S0或S1

  • 之后,每经历过一次新生代回收,对象如果存活,它的年龄就会加一。

  • 当对象的年龄达到一定条件后,就会被认为是老年代对象,从而进入老年代。

新生代

存放新创建的对象, GC 频繁但快速

  • Eden 区:新对象首先分配在这里

  • Survivor 区:两个大小相等的区域 S0、S1,用于存放 Eden 区 GC 后存活的对象

老年代

存放长期存活的对象或大对象,如大数组

GC 频率低,但耗时长

方法区

是 JVM 规范,所有虚拟机必须遵守

  • 线程共享

  • 逻辑上属于堆,但不要求物理连续,也不一定在堆内

  • 垃圾回收行为不确定,规范未强制要求 GC 方法区,但主流 JVM 会回收

  • 内存不足会 OOM

方法区是 JVM 规范定义的概念,永久代/元空间都是其具体实现

保存内容:

  • 类的元数据,参考类加载流程中的加载部分。

  • 运行时常量池 .class 文件中常量池的运行表示,如字面量、符号引用等

  • 字段和方法数据

  • 静态变量 类的 static 字段。JDK 7后,其本身在堆中,但其引用或元信息仍在方法区

  • JIT 编译后的代码缓存 热点方法被 JIT 编译为本地代码后缓存于此

实现

  • 永久代

    JDK 7 以及之前使用永久代,位于堆内,受 -Xmx 限制。容易因动态生成类太多而 OOM。GC 不会在主程序运行期间对永久区域进行清理,Full GC 才会清理,因此容易因动态生成类太多而产生 OOM

  • 元空间 metasapce

    不再位于堆中,而是使用操作系统本地内存。默认只受系统可用内存限制,降低 OOM 风险。但如果不指定最大大小,可能耗尽系统所有可用内存。

元空间的优势:

  • 不再位于 Java 堆,而是使用 操作系统本地内存(Native Memory)。

  • 默认只受系统可用内存限制,大幅降低 OOM 风险。

  • 类的元数据按类加载器隔离管理,卸载更高效。

  • 支持更灵活的内存管理(如按类加载器释放)。

虚拟机栈与本地方法栈

存储 方法执行时的栈帧(Stack Frame),包括局部变量表、操作数栈、动态链接、方法返回地址等。

线程私有,生命周期与线程相同。由于私有,因此栈中的数据是线程安全的,无需同步。

栈帧是核心单位。每调用一个方法,就会创建一个栈帧并压入栈顶。方法执行完毕后,栈帧出栈。

局部变量表

储存 方法参数 和方法内部的 局部变量 。大小在编译器确定。

操作数栈

用于保存中间计算结果,作为字节码指令的操作数。

例如:执行 i = a + b 时:

  1. 先将 ab 压入操作数栈
  2. 执行 iadd 指令,弹出两个值相加,结果再压入栈
  3. 最后通过 istore 存入局部变量表

局部变量表和操作数栈都是栈帧中的 逻辑结构

动态链接(常量池指针)

指向运行时常量池中该方法所属类的引用,用于支持方法调用过程中的符号解析

方法返回地址

当前方法执行完后,应该返回到调用者的哪条指令继续执行。正常返回或异常退出都会使用此信息。

本地方法栈

为 native 方法服务,与虚拟机栈类似。

对比项虚拟机栈本地方法栈
服务对象Java 方法Native 方法(C/C++ 编写)
规范要求必须支持可自由实现(有些 JVM 合并两者)
异常类型StackOverflowError 等类似,但取决于 native 实现

程序计数器

记录当前线程正在执行的字节码指令地址。如果是 native 方法,为 undefined.

线程私有;不会发生 OOM ; 字节码解释器通过改变 PC 值来选取下一条指令。

JVM 类加载流程