jvm中有一个比较重要的就是程序计数器,简单记录一下。
程序计数器(Program Counter Register,PC 寄存器) 记录的字节码指令地址是 在编译阶段确认好的,但在运行阶段会根据程序的执行流程动态变化。
1. 字节码指令的存储位置
1.1 字节码指令的来源
编译阶段:
Java 源代码在编译阶段会被编译成字节码(.class 文件)。
字节码是一种中间代码,与平台无关,由 JVM 解释执行或即时编译(JIT)为机器码。
运行阶段:
JVM 加载 .class 文件,将字节码指令加载到 方法区(Method Area) 中。
字节码指令在方法区中以方法为单位存储。
1.2 字节码指令的存储位置
方法区(Method Area):
字节码指令存储在方法区中,每个方法对应一段字节码指令。
方法区是所有线程共享的内存区域。
运行时常量池(Runtime Constant Pool):
字节码指令中可能包含对常量池的引用(如字符串常量、类名、方法名等)。
运行时常量池是方法区的一部分。
2. 程序计数器的作用
2.1 程序计数器的功能
记录执行位置:
程序计数器记录当前线程正在执行的字节码指令地址。
线程切换恢复:
在线程切换时,程序计数器保存当前线程的执行位置,以便恢复执行。
分支、循环、异常处理:
在分支、循环和异常处理时,程序计数器会跳转到指定的字节码地址。
2.2 程序计数器的内容
字节码指令地址:
程序计数器存储的是字节码指令的地址,而不是字节码指令本身。
字节码指令地址是相对于方法起始地址的偏移量。
动态变化:
程序计数器的值在运行阶段会根据程序的执行流程动态变化。
例如,在方法调用、分支跳转、循环和异常处理时,程序计数器的值会更新。
3. 字节码指令地址的确认
3.1 编译阶段
字节码生成:
在编译阶段,Java 编译器将源代码编译成字节码,并生成 .class 文件。
字节码指令的地址在编译阶段已经确定,并以偏移量的形式存储在 .class 文件中。
常量池:
字节码指令中可能包含对常量池的引用(如方法名、类名、字符串常量等)。
常量池的索引在编译阶段已经确定。
3.2 运行阶段
字节码加载:
JVM 加载 .class 文件,将字节码指令加载到方法区中。
字节码指令的地址在方法区中是固定的。
程序计数器更新:
在运行阶段,程序计数器根据字节码指令的执行流程动态更新。
例如,在方法调用时,程序计数器跳转到被调用方法的字节码起始地址。
4. 示例
以下是一个简单的 Java 方法示例,展示字节码指令的生成和程序计数器的作用:
4.1 Java 代码
public class Example {
public static void main(String[] args) {
int a = 1;
int b = 2;
int result = add(a, b);
System.out.println(result);
}
public static int add(int x, int y) {
int sum = x + y;
return sum;
}
}
4.2 字节码指令
通过 javap -c Example 可以查看字节码指令:
public class Example {
public static void main(java.lang.String[]);
Code:
0: iconst_1 // 将常量 1 压入操作数栈
1: istore_1 // 将栈顶值存储到局部变量表索引 1(a)
2: iconst_2 // 将常量 2 压入操作数栈
3: istore_2 // 将栈顶值存储到局部变量表索引 2(b)
4: iload_1 // 加载局部变量表索引 1 的值(a)
5: iload_2 // 加载局部变量表索引 2 的值(b)
6: invokestatic #2 // 调用 add 方法
9: istore_3 // 将栈顶值存储到局部变量表索引 3(result)
10: getstatic #3 // 获取 System.out 的引用
13: iload_3 // 加载局部变量表索引 3 的值(result)
14: invokevirtual #4 // 调用 println 方法
17: return // 返回
public static int add(int, int);
Code:
0: iload_0 // 加载局部变量表索引 0 的值(x)
1: iload_1 // 加载局部变量表索引 1 的值(y)
2: iadd // 将栈顶两个值相加
3: istore_2 // 将栈顶值存储到局部变量表索引 2(sum)
4: iload_2 // 加载局部变量表索引 2 的值(sum)
5: ireturn // 返回栈顶值
}
4.3 程序计数器的变化
main 方法执行:
程序计数器从 0 开始,逐条执行字节码指令。
在调用 add 方法时,程序计数器跳转到 add 方法的字节码起始地址。
add 方法执行:
程序计数器从 0 开始,逐条执行 add 方法的字节码指令。
在 add 方法返回时,程序计数器跳转回 main 方法的下一条指令地址。
5. 总结
程序计数器是在编译成字节码的时候确认的,意味着在运行态是不可变的,反馈到底层上面,就是一堆寄存器汇编指令,进行方法的跳转,偏移以及赋值,其中的内存地址在发生变化而已。