持续更新JVM相关知识,敬请关注:Java虚拟机精髓专栏zhuanlan.zhihu.com
程序计数寄存器(Program Counter Register),关于他的相关内容,最权威的当然是官方的介绍,大家可以从下面的网址直接查看Oracle官方的文档,这里是最准确全面的解释:The Java® Virtual Machine Specificationdocs.oracle.com
如果英文不好,无法透彻理解,那么请继续往下看:
程序计数寄存器,英文是Program Counter Register,叫起来有些长,绕口,平时也会简化叫做程序计数器或PC寄存器。
这里叫他寄存器,和硬件层面的物理寄存器并不是一回事儿,他只是对物理寄存器的一种软件模拟。其实叫他程序计数器更加的合理,免得发生不必要的混淆。也可以叫他钩子。
那么这个钩子钩的是什么呢?或者说他计数计的是什么数呢?
如上图,程序运行时是一个个线程执行,线程中有着要执行的一行行代码,这一个个钩子就勾着一行行的待执行代码,他也成为行号指示器,一行代码执行完了,下一行要执行哪个了,就由他来标记。说的官方一点儿,就是程序计数器,是用来存储指向下一条指令的地址,也就是将要执行的指令代码。程序计数器所占用的内存空间很小,可以忽略,他也是运行速度最快的存储区域。
程序计数器每个线程都独立有一份,随着线程的生命周期
同一时间,一个线程内只有一个方法在执行,称为当前方法,程序计数器就是存储着当前方法的JVM指令地址,如果执行的是native方法,则值是undefined
程序中所有的跳转、循环、异常处理等都依赖于程序技术器来完成
他是JAVA虚拟机规范中唯一一个没有规定任何内存溢出情况的区域
下面我们看个更直观的例子,首先我们先写一段代码,不多讲,谁都能看懂:
public static void main(String[] args) {
int i=1;
int j=2;
int k=i+j;
}
然后使用javap命令反编译class文件,得到如下内容:
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: iconst_1 //常量1 1: istore_1 //保存到索引为1的位置 2: iconst_2 //常量2 3: istore_2 //保存到索引为2的位置 4: iload_1 //取出索引位置1数据 5: iload_2 //取出索引位置2数据 6: iadd //做加法运算 7: istore_3 //结果保存到索引为3的位置 8: return //程序结束 LineNumberTable:
line 4: 0
line 5: 2
line 6: 4
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
2 7 1 i I
4 5 2 j I
8 1 3 k I
我们可以看到,反编译后的程序执行过程,每一个执行过程前面都有一个数字,这些数字就是指令地址,也叫偏移地址,这也就是程序计数器所存储的结构,数字后面的内容叫做代码指令。
执行引擎根据程序计数器中的指令地址,来取出相应的操作指令去执行当前应该执行的操作。如下图所示:
面试时关于程序计数器经常会被问到一个问题,就是为什么要用程序计数器来存储指令地址呢?
如果你对上面的知识都听懂了,那么答案也很简单:
因为CPU的多核心在执行多线程程序时,并不是真的多个线程同时执行,而是在多个线程中快速的进行切换执行,因为同一时间只能操作一个线程,那么当切换回某个线程时,CPU需要明确知道程序执行到什么位置了,该从哪继续执行,那么这时候,就需要程序计数器来告诉他,下一条需要执行的指令地址是什么。
说到这里,再说一下CPU时间片的概念,我们日常在使用计算机时,同时会打开很多程序,无论是多个CPU还是一个CPU多核心,还是一个CPU单一核心,都可以做到如此,所有程序“同时”在运行。这里注意“同时”两个字我用了引号,那也就是说并非同时,根据上一节的知识我们知道,CPU同一时间只能处理一个线程,那么又是如果做到看似很多程序一起执行呢?
这就利用了一个叫做CPU时间片的概念,CPU给每个线程非配一段执行时间,时间到了就切换到下一个线程,由于CPU的主频非常高,切换速度非常快,以至于宏观上的感受就是程序一起在运行。实际微观层面,是轮流执行的。这个大家要理解。