程序计数器的英文全称是Program Counter Register,又叫程序计数寄存器。Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
即在物理上实现程序计数器是通过一个叫寄存器来实现的,我们的程序计数器是Java对物理硬件的屏蔽和抽象,他在物理上是通过寄存器来实现的。寄存器可以说是整个CPU组件里读取速度最快的一个单元,因为读取/写指令地址这个动作是非常频繁的。所以Java虚拟机在设计的时候就把CPU中的寄存器当做了程序计数器,用他来存储地址,将来去读取这个地址。
用于存储下一条指令的地址。详细的说PC寄存器是用来存储指向下一条指令的地址,也就是即将将要执行的指令代码。由执行引擎读取下一条指令。
即Java源代码不能直接去被执行,他得经过一次编译,编译成二进制字节码,里面的一行一行的东西都是JVM指令,Java虚拟机跨平台的基础就是这些JVM指令,这些指令在所有平台都是一致的。但这些指令也不能直接交给CPU执行,他必须要经过一个解释器,这个解释器也是Java虚拟机执行引擎的一个组件,他就专门负责把每一条JVM指令(比如getstatic)解释成为机器码,机器码就可以交给CPU执行。
比如下面例子中的左侧是JVM指令,右侧是相应的源代码。
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
比如在这里PrintStream out = System.out;代码对应着第一行和第二行指令,其他省略。
JVM指令的执行流程
Java中虚拟机指令的这些执行流程,即拿到一条指令,交给解释器,
解释器把他翻译成机器码,机器码才能交给CPU来运行。
程序计数器的作用是在一些指令的执行过程中,记住下一条JVM指令的执行地址。比如,上面的例子中,JVM指令前面都有数字,这些数字可以理解成指令对应的地址,当这些指令被虚拟机加载到内存以后,地址就会跟上面的数字是类似的,根据这个地址信息可以找到命令执行它。
执行流程加上程序计数器后的样子
比如拿到了第一条getstatic指令,交给了解释器,解释器把他变成机器码,
然后再交给CPU运行,但是在与此同时,他就会把下一条指令即下面的astore_1
指令的地址即把3放入程序计数器。
所以等第一条指令执行完了以后,解释器就会到程序计数器里去取下一条指令,
根据地址3再找到下调指令astore_1,然后再重复刚才的过程。
当然,在执行3这条指令的时候,再把3的下一条即例子中的4存入程序计数器。
总之,他记录了下一个JVM的指令地址。
所以,如果没有程序计数器,他就不知道接下来该执行哪条命令了,这是程序计数器的基本作用。
使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始
继续执行JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行
什么样的字节码指令
程序计数器为什么被设定为线程私有?
为了能够准确记录各个线程正在执行的当前字节码指令地址,最好的办法就是为每
一个线程分配一个程序计数器。每个线程在创建后,都会产生自己的程序计数器和
栈帧,程序计数器在各个线程之间互不影响。
为什么在执行native本地方法时,程序计数器的值为空(Undefined)?
因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法
相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。
由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并
且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。