【JVM】Java程序运行时数据区

发布于:2025-06-02 ⋅ 阅读:(28) ⋅ 点赞:(0)

运行时数据区

运行时数据区是Java程序执行过程中管理的内存区域

在这里插入图片描述

Java 运行时数据区组成(JVM 内存结构)

Java 虚拟机(JVM)的运行时数据区由以下核心部分组成:

线程私有:程序计数器、Java虚拟机栈、本地方法栈。

线程共享:方法区、堆。

在这里插入图片描述

一、程序计数器

程序计数器存储当前执行的字节码指令地址。

线程私有:每个线程都有独立的程序计数器。

多线程场景下程序计数器的工作流程:

Thread1 CPU Thread2 执行指令 [PC=15] 时间片用完 保存PC值(15) 恢复执行 [PC=22] 执行指令... 时间片用完 保存PC值(25) 恢复执行 [PC=15] 继续执行下条指令 Thread1 CPU Thread2

常见问题解答

Q:为什么程序计数器不会内存溢出?
A:其存储的是单个指令地址(指针),非用户数据。指针长度固定(32位系统4字节/64位8字节),且线程结束时自动释放。

Q:PC 如何影响程序流程控制?
A:分支指令直接修改 PC 值实现跳转:

0: iload_1
1: ifeq 12      // 如果值为0,PC跳转到12
4: iinc 1, -1   // 否则继续执行
...
12: return      // 跳转目标

Q:调试器如何实现断点功能?
A:通过修改 PC 值实现:

  1. 在目标指令处插入特殊断点指令
  2. 当 PC 指向断点时暂停执行
  3. 显示当前堆栈和变量状态

二、Java虚拟机栈

Java虚拟机栈描述的是Java方法执行过程中的线程内存模型,主要作用是管理Java方法的调用过程。在当前线程中每个方法被调用的时候,JVM会同步创建一个栈帧呀入到虚拟机栈。栈帧中存储着局部变量表、操作数栈、动态链接、方法返回地址等。

栈帧组成

1. 局部变量表
  • 存储内容

    • this(实例对象的地址)

    • 方法参数

    • 局部变量

    • 基本类型数据(int, boolean 等)

    • 对象引用(reference)

      在这里插入图片描述

  • 容量单位变量槽(Slot)

    • 32位类型占1个Slot(int, float, reference

    • 64位类型占2个Slot(long, double

      索引 类型 名称 大小 示例值
      0 reference this 1槽 0x00a3b1
      1 int a 1槽 10
      2 double b 2槽 20.5
      4 reference c 1槽 0x00c4d2
      5 long d 2槽 100
  • 内存复用:Slot在作用域结束后可被复用

public void demo(int param) {
    int a = 10;          // Slot 0: this | Slot 1: param | Slot 2: a
    double b = 20.0;     // Slot 3-4: b (占2个Slot)
    String s = "hello";  // Slot 5: s
}
2. 操作数栈
  • 作用:执行字节码指令的工作区

  • 深度:编译期确定(写入方法表的 max_stack 属性)

  • 示例代码及执行过程

    以下面的 Java 代码为例,分析操作数栈在方法执行过程中的具体工作情况。

    public class OperandStackExample {
        public static int add(int a, int b) {
            int c = a + b;
            return c;
        }
    
        public static void main(String[] args) {
            int result = add(3, 5);
            System.out.println(result);
        }
    }
    
    编译后的字节码分析

    add 方法编译后的部分字节码如下:

    public static int add(int, int);
      Code:
         0: iload_0         // 将局部变量表中第 0 个位置的 int 型变量(参数 a)压入操作数栈
         1: iload_1         // 将局部变量表中第 1 个位置的 int 型变量(参数 b)压入操作数栈
         2: iadd            // 从操作数栈中弹出两个 int 型操作数,相加后将结果压入操作数栈
         3: istore_2        // 将操作数栈顶的 int 型结果弹出,存入局部变量表中第 2 个位置(变量 c)
         4: iload_2         // 将局部变量表中第 2 个位置的 int 型变量(变量 c)压入操作数栈
         5: ireturn         // 将操作数栈顶的 int 型结果返回
    
    操作数栈状态变化
    1. 执行 iload_0 指令后:将参数 a(值为 3)压入操作数栈,此时操作数栈栈顶元素为 3。
    2. 执行 iload_1 指令后:将参数 b(值为 5)压入操作数栈,此时操作数栈从栈顶到栈底元素依次为 5、3。
    3. 执行 iadd 指令后:从操作数栈中弹出 5 和 3,计算 3 + 5 = 8,将结果 8 压入操作数栈,此时操作数栈栈顶元素为 8。
    4. 执行 istore_2 指令后:将操作数栈顶的 8 弹出,存入局部变量表中变量 c 的位置,此时操作数栈为空。
    5. 执行 iload_2 指令后:将局部变量表中变量 c 的值 8 压入操作数栈,此时操作数栈栈顶元素为 8。
    6. 执行 ireturn 指令后:将操作数栈顶的 8 返回,方法执行结束。
3. 动态链接
  • 存储内容:指向方法区运行时常量池的引用
  • 核心作用:将符号引用解析为直接引用
    • 类方法调用:确定目标方法的入口地址
    • 字段访问:定位字段在内存中的偏移量
4. 方法返回地址
  • 两种返回方式
    • 正常返回:PC计数器值作为返回地址
    • 异常退出:异常处理器表记录的地址
  • 关键动作
    • 恢复上层方法的局部变量表
    • 将操作数栈结果压回调用者栈帧
    • 调整PC计数器

三、本地方法栈

本地方法栈是 JVM 为执行本地方法(Native Method)提供的内存区域。本地方法是使用非 Java 语言(如 C、C++)实现的方法,主要作用是管理本地方法的调用过程。

栈帧结构

本地方法栈的栈帧结构和 Java 虚拟机栈的栈帧类似,通常包含以下部分:

局部变量表:用于存储本地方法执行过程中的局部变量,包括基本数据类型和对象引用。
操作数栈:在本地方法执行计算时,用于存储操作数和中间结果。
动态链接:将符号引用转换为直接引用,以便在运行时能够正确调用方法。
方法返回地址:记录方法执行完毕后返回的位置。

以上都是线程不共享,每个线程私有的内存区域,与每个线程当前执行位置与内存模型息息相关,都在线程创建时创建,在线程销毁时销毁。

四、方法区

方法区存放着已经被虚拟机加载的类型信息、常量、静态变量等数据。

储存内容

  • 类型信息:

    • 类的全限定名(如 java.lang.String
    • 类的直接父类的全限定名(对于 Object,没有父类)
    • 类的修饰符(public, abstract, final 等)
    • 实现的接口列表
    • 字段信息(字段名称、类型、修饰符)
    • 方法信息(方法名称、返回类型、参数类型/数量、修饰符、字节码、操作数栈和局部变量表大小)
  • 运行时常量池:

    • 是字节码文件中常量池的运行时表现形式,被每个类或接口所独有。
    • 包含:
      • 编译期已知的字面量:文本字符串、final 常量值。
      • 符号引用:类和接口的完全限定名、字段的名称和描述符、方法的名称和描述符。这些符号引用在类加载的解析阶段会被转化为直接引用(如内存地址)。

    JDK 7 之后,字符串常量池从方法区移到了堆中,但其他类型的常量(如整数常量、浮点常量等)依然存于运行时常量池,位于元空间。

  • 静态变量:

    • 静态变量存储在方法区,可供所有的实例访问。

实现演变

  • 永久代(JDK7及以前):方法区使用永久代实现,永久代使用堆中的一部分内存,有固定大小限制,容易发生内存溢出。
  • 元空间(JDK8以后):移除永久代,使用元空间实现方法区,元空间使用系统直接内存,不再受堆内存的限制。

五、堆

在JVM中,堆的作用是存放所有的对象实例。堆被所有的线程共享。

存储内容

1.对象实例

  • 所有new创建的对象。
  • 数组对象:
    • int[], double[] 等基本类型数组(数组对象本身及其元素值都在堆中连续空间中)。
    • String[], Object[] 等引用类型数组(数组对象本身在堆中,其元素是指向堆中其他对象的引用)。

易混淆点

  1. 字符串对象与字符串常量池
    • 字符串对象本身(如 new String("abc") 或运行时拼接生成的 String存储在堆中
    • 字符串常量池自 JDK 7 起移至堆中,它存储的是:
      • 字符串字面量(如 "abc")的引用(指向堆中的 String 对象)。
      • String.intern() 方法返回的字符串的引用。
    • 总结:字符串对象在堆,常量池(StringTable)也在堆(存储引用),但常量池本身是一个哈希表结构。
  2. 静态变量引用的对象
    • 静态变量(static 修饰)的引用本身存储在方法区(JDK 7+ 的元空间)。
    • 静态变量指向的对象实例(如 static Object obj = new Object(); 中的 new Object()存储在堆中
  3. **类信息:
    • 类的元数据(如类名、方法字节码、字段结构等)存储在方法区(元空间)不在堆中
    • 类的 Class 对象(如 String.class)是一个特殊的对象实例,存储在堆中
  4. 基本类型局部变量 vs. 成员变量
    • 成员变量(在对象内部):基本类型(如 int, double)的值直接存在堆内对象内存中。
    • 局部变量(在方法内部):基本类型(如 int i = 10;)的值存储在 Java 栈的栈帧的局部变量表 中,不在堆中

网站公告

今日签到

点亮在社区的每一天
去签到