Java虚拟机 - 程序计数器和虚拟机栈
运行时数据结构
- Java运行时数据区
- 程序计数器
- 为什么需要程序计数器
- 执行流程
- 虚拟机栈
- 虚拟机栈作用
- 虚拟机栈核心结构
- 运行机制
Java运行时数据区
首先介绍Java运行时数据之前,我们要了解,对于计算机来说,内存是非常重要的资源,因为内存是连接CPU与硬盘的桥梁,承载着操作系统与应用程序的运行的基础。JVM在运行期间把它管理的内存分为若干个区域,有些区域是线程私有的,有些区域是共享的。
Java运行时数据区作为JVM在程序执行过程中管理内存的核心结构,主要包括方法区(存储类元数据、运行时常量池、静态变量)、堆(存放对象实例和数组,被所有线程共享且是垃圾回收的主区域)、虚拟机栈(每个线程私有,用于存储方法调用的栈帧,包含局部变量表、操作数栈及方法出口)、本地方法栈(支持Native方法调用)和程序计数器(记录当前线程执行的字节码位置,确保多线程切换后能恢复执行)。其中,堆和方法区是线程共享的,而虚拟机栈、本地方法栈和程序计数器为线程私有,共同协作实现Java程序的内存分配、方法执行及多线程调度。
程序计数器
程序计数器(Program Counter Register)在JVM中可以当成当前线程所执行的字节码的行号指示器,在JVM的概念模型里面,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器。分支 、循环 、跳转 、异常处理 、线程恢复等都是依赖这个计数器来实现的。
如果程序执行的是Java方法,计数器代表的是只在执行的虚拟机字节码的地址;如果执行的是本地方法,这个计数器的值为空(undefined)。此外,这个计数器区域是Java虚拟机规范的没有规定任何内存溢出的地方,程序计数区既没有内存溢出,也没有垃圾回收。因为程序计数器是很小的一块内存区域,几乎可以忽略不记,同时也是运行最快的区域。
为什么需要程序计数器
因为CPU需要不停的切换各个线程,做完切换之后,需要知道接下来从哪里开始执行。通过使用程序计数器保存了接下来要执行的地址,这样CPU切换过来之后就可以直接接着执行。
执行流程
// 示例代码
public class Demo {public int test() {int a = 10;int b = 20;int c = a + b;String d = "abd";System.out.println(d);return d.length();}
}
通过javap -c Demo.class反编译后:
0 bipush 102 istore_13 bipush 205 istore_26 iload_17 iload_28 iadd9 istore_3
10 ldc #2 <abd>
12 astore 4
14 getstatic #9 <java/lang/System.out : Ljava/io/PrintStream;>
17 aload 4
19 invokevirtual #15 <java/io/PrintStream.println : (Ljava/lang/String;)V>
22 aload 4
24 invokevirtual #21 <java/lang/String.length : ()I>
27 ireturn
虚拟机栈
虚拟机栈(Java Virtual Machine Stack)是JVM内存模型中与线程执行密切相关的核心区域,用于存储方法调用的栈帧(Stack Frame)。它是线程私有的内存空间,每个线程在创建时都会分配一个独立的虚拟机栈,其生命周期与线程一致。以下从设计目标、核心结构、运行机制到常见问题全面解析。
虚拟机栈作用
虚拟机栈的核心功能是支持Java方法的调用与执行,具体包括:
-
方法调用链管理:保存方法的调用顺序(如 main() → methodA() → methodB())。
-
局部变量存储:存储方法内的基本类型变量、对象引用。
-
操作数计算:提供临时数据存储空间(如算术运算的中间结果)。
-
方法返回控制:记录方法执行完成后的返回地址。
虚拟机栈核心结构
虚拟机栈由多个栈帧(Stack Frame)构成,每个栈帧对应一个方法的调用。栈帧包含以下核心部分:
-
局部变量表(Local Variables Table)
作用:存储方法参数和方法内定义的局部变量。结构:以变量槽(Slot)为最小单位,每个Slot占用32位(long和double占2个Slot)。索引从0开始,依次存放this(非静态方法)、方法参数、局部变量。示例:
public void demo(int a, String b) {double c = 3.14;Object d = new Object();
}
局部变量表结构:
-
操作数栈(Operand Stack)
作用:保存计算过程中的临时数据(类似CPU的寄存器)。
特点:
深度在编译期确定(写入方法表的max_stack属性)。通过iconst_1、iadd等字节码指令操作栈顶元素。
int result = 1 + 2;
iconst_1 // 压入1
iconst_2 // 压入2
iadd // 弹出1和2,相加后压入3
istore_1 // 将3存储到局部变量表索引1
- 动态链接(Dynamic Linking)
-
作用:将符号引用(如com/example/Demo.methodA)转换为直接引用(内存地址)。
-
意义:支持多态特性(如接口方法、虚方法调用)。
-
对比:
-
静态解析:类加载阶段可确定的直接引用(如final方法)。
-
动态链接:运行时才能确定(如重写方法)。
-
- 方法返回地址(Return Address)
作用:记录方法正常结束或异常退出后的返回位置。
两种返回方式:
-
正常返回(return指令):程序计数器恢复为调用者的下一条指令地址。
-
异常退出:通过异常处理器表(Exception Table)确定跳转地址。
运行机制
- 方法调用与栈帧压栈
调用方法时:创建新栈帧并压入栈顶。
方法返回时:栈帧弹出,释放内存。
- 栈溢出(StackOverflowError)
触发条件:线程请求的栈深度超过JVM允许的最大值(如无限递归)。
示例:
public class StackOverflowDemo {public static void main(String[] args) {infiniteCall(); // 无限递归调用}static void infiniteCall() {infiniteCall();}
}
报错信息:
Exception in thread "main" java.lang.StackOverflowError
- 栈大小配置
参数:-Xss(如-Xss1m设置栈大小为1MB)。
默认值:不同JVM实现不同(HotSpot Linux x64默认1MB)。