JVM 的运行时数据区

本文最后更新于 2025年8月10日 19:10

image.png

我们可以将整个运行时数据区分为 线程私有区域线程共享区域 这两个部分。下面简单介绍这两个区域所包含的部分及其功能:

  • 线程私有区域:
    • 程序计数器 PC :当前线程执行字节码行号的指示器
    • 虚拟机栈:为除了 Native 方法以外的 Java 方法服务
    • 本地方法栈:为 Java 方法服务
  • 线程共享区域:
    • 堆:存放几乎全部的对象实例
    • 方法区:存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

程序计数器 PC

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

如果线程正在执行一个 Java 方法,那么 PC 记录的是正在执行的虚拟机字节码指令的地址;如果线程正在执行一个 Native 方法,那么 PC 的值应该为 空(Undefined)

虚拟机栈

除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过虚拟机栈来实现的。栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

  • 局部变量表:存放了编译期可知的各种数据类型
  • 操作数栈:用于存放方法执行过程中产生的中间计算结果和临时变量
  • 动态链接:用于在一个方法调用其他方法时,将符号引用转换为内存地址的直接引用
  • 方法返回地址:当前栈帧执行结束后要返回的地址

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

为什么说是“几乎所有”呢?因为从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

对堆的具体讲解可以查看:https://moonike.cloud/2025/05/19/浅谈 JVM 堆内存/

方法区

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。那为什么要将方法区的实现由永久代变为元空间呢?可以从以下几点考虑:

  • 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整(也就是受到 JVM 内存的限制);而元空间使用的是本地内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

JVM 的运行时数据区
http://example.com/2025/03/24/JVM 的运行时数据区/
作者
Moonike
发布于
2025年3月24日
更新于
2025年8月10日
许可协议