目录
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的信息。CPU只有把数据装载到寄存器才能够运行。但是这里并非是广义上所指的物理寄存器,在JVM中只是对PC寄存器的一种模拟,用来处理当前线程相关指令的计数器。
有一点与CPU的寄存器是类似的,那就是占用空间小,但运行速度最快。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,因此它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
在JVM规范中,每个线程都有它自己的程序计数器,而且是私有的,生命周期也与线程的生命周期一致。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。
需要注意的是PC区是唯一一个没有OutofMemoryError情况的区域。而本地栈等结构没有垃圾回收,但是有可能溢出。
本文我们将介绍PC计数器是如何工作的,如何表示跳转和循环等操作的,以及线程安全相关的问题。
1 功能演示
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
编译之后,执行javap -v PCRegisterTest.class,查看字节码,其中与PC有关的是:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
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
上面的竖排0 到10,就可以理解为PC计数器,而右侧的istore_1等就是操作指令,如下图。
执行的时候如果PC发出的指令序号是5,那么这里就会执行存储操作,然后会执行操作局部变量表,操作数栈等等,最后还要再转换成机器指令给CPU来执行。
现在有个问题 ,PC计数器怎么知道要执行哪条指令的呢?PC计数器的值是谁来改的呢?修改PC值的是执行引擎。如下图所示。每执行一次,执行引擎就会修改该值来通知当前线程接下来要做什么工作。可以简单将PC理解为一个调度员。
2 跳转、循环等执行的执行原理
如果我们增加代码,会发现上面的Code下的指令会持续增加,对应的就是将每条语句都处理成了能够执行的指令。我们现在思考一个问题跳转、循环等功能,PC指令如何表示以及执行的呢?
先说结论:跳转还是循环是在字节码的指令里设置好的,PC计数器执行的时候不管这些,只关心接下来要执行哪一条指令。
我们先看一下跳转结构:
public class PCRegisterTest {
public static void main(String[] args) {
int i = 30;
if (i < 30) {
i++;
} else {
i--;
}
}
}
我们通过javap -v来查看生成的PCRegisterTest.class文件的信息:
可以看到,这里的指令6是一个比较指令
6: if_icmpge 15
表示的含义是,如果比较只会满足要求就继续向下执行,否则就跳转到指令15执行。
我们可以看到,如果满足条件继续执行,那就是iinc
就是累加1指令。如果跳到指令15,那就是iinc
指令执行累减1指令,因此与我们的代码设计是一致的。
我们再来看一下循环的情况:
public class PCRegisterTest {
public static void main(String[] args) {
int i = 30;
int j = 40;
for (i = 10; i < j; ) {
int k = i + j;
}
}
}
在main()方法位置会看到如下信息:
可以看到,生成的指令里,11号位置有个比较,如果满足才会继续向下走,如果不满足就会直接跳到21行。
我们注意到11行之下的正好是循环体的内容,而21行正好是退出。因此这里PC计数器仍然可以无脑向下走,不用关心是否为循环,只要知道下一个位置就可以了。
我们再看一下do while循环是否也这样:
public class PCRegisterTest {
public static void main(String[] args) {
int i = 30;
do {
i++;
} while (i < 40);
}
}
生成的指令如下:
可以看到,指令9有个比较指令if_icmplt,如果满足就跳到指令3位置继续执行,否则就继续执行指令12。而指令3的位置正好就是累加操作i++,因此完美实现了循环。
3 关于PC的面试题
问题1: 使用PC寄存器存储字节码指令地址有什么用呢?
这是因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。具体来说就是JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2.PC寄存器为什么被设定为私有的?
所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。