JVM - 程序计数器
# 1. 介绍
官方文档地址:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
JVM 中的程序计数寄存器(Program Counter Register)名称源于 CPU 的寄存器。寄存器存储指令相关的现场信息。CPU 只有将数据装载到寄存器才能运行。这里的寄存器并非广义上的物理寄存器,而是对物理 PC 寄存器的一种抽象模拟,翻译为 PC 计数器或指令计数器会更贴切。
它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
任何时间一个线程都只有一个方法在执行,也就是所谓的 当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;如果是在执行 native 方法,则是未指定值(undefined)。
程序计数器是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器完成。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
它是唯一一个在 Java 虚拟机规范中 没有规定任何 OutofMemoryError 情况的区域。
笔记
程序计数器相当于行号计数器,记录下一行的地址,方便程序继续执行。类似于 JDBC 结果集的游标和集合的迭代器指针
# 2. 作用
PC 寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎或 CPU 读取下一条指令。
# 3. 代码演示
我们首先写一个简单的代码
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
String s = "abc";
System.out.println(i);
System.out.println(k);
}
}
2
3
4
5
6
7
8
9
10
11
然后将代码编译成字节码文件:
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
2
3
4
5
6
7
8
9
上方左边的数字代表 指令地址(指令偏移),用于指向当前执行到哪里,即 PC 寄存器中可能存储的值,然后执行引擎读取 PC 寄存器中的值,并执行该指令。
通过 PC 寄存器,我们就可以知道当前程序执行到哪一步了
# 4. 使用PC寄存器存储字节码指令地址有什么用呢?
CPU 需要不停地切换各个线程,而每次切换回来后需要知道从哪一条指令继续执行。JVM 的字节码解释器通过改变 PC 寄存器的值来明确下一条应该执行的字节码指令。这种机制确保了线程在切换时能够精确地记录和恢复其执行状态。
# 5. 两个面试题
# 5.1 为什么PC寄存器是私有的?
每个线程在创建后都会有自己的程序计数器(PC寄存器)和栈帧。PC寄存器在各个线程之间互不影响。这种设计保证了线程的独立性和不被其他线程干扰。
原因:
- 多线程切换:在一个特定的时间段内,只会执行某一个线程的方法。CPU 不停地切换任务,导致经常中断和恢复。
- 准确记录指令地址:为了准确地记录各个线程正在执行的当前字节码指令地址,最好的办法是为每一个线程分配一个 PC 寄存器。这样,各个线程之间便可以独立计算,不会相互干扰。
如上图所示:PC 寄存器 1 执行到了指令地址 5,然后切换到 PC 寄存器 2 执行到 7,接着切换到 PC 寄存器 3 执行到 17,最后再切换回 PC 寄存器 1 执行 5 后面的代码。如果共用一个 PC 寄存器,指令地址会被覆盖,导致执行混乱。
# 5.2 什么是CPU时间片?
CPU 时间片即 CPU 分配给各个程序的时间段。每个线程被分配一个时间段,称作时间片。时间片轮转调度是操作系统中常用的多任务调度方式。
宏观上:我们可以同时打开多个应用程序,每个程序似乎并行运行。
微观上:由于只有一个 CPU,一次只能处理一个程序的部分任务。引入时间片后,每个程序轮流执行,确保公平。
总结
- PC寄存器的意义:存储指向下一条指令的地址,确保线程切换后能准确恢复执行状态。
- PC寄存器私有性:每个线程有自己的 PC 寄存器,独立记录执行位置,避免被其他线程干扰。
- CPU时间片:分配给每个程序的时间段,确保多任务的公平调度和执行。
通过理解 PC 寄存器的作用、私有性及 CPU 时间片机制,可以更好地掌握多线程编程和 JVM 的执行原理。