JVM 程序计数器

  • Post author:
  • Post category:其他

概念

程序计数器的英文全称是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寄存器的值来明确下一条应该执行
什么样的字节码指令

特点

  1. 线程私有
    Java程序是支持多线程运行的,比如,刚才例子中的那些指令在线程1里运行,同时可能还运行着其他的线程2线程3等等,在这种多个线程运行的时候,CPU会有一个调度器组件,给他们分配时间片,比如说给线程1分配一个时间片,若在时间片内,线程1的代码没有执行完,他就会把线程1的状态执行暂存,切换到线程2去,因为不能让线程2老等着,等线程2代码执行到一定程度了,线程2的时间片用完了,那么切换回来再继续执行线程1的剩余部分的代码,这是时间片的概念。那如果在线程切换的过程中,如果我要记住下一条指令执行到哪里了,那么还是要用到程序计数器,比如线程1执行到了第九行代码,恰巧这个时候他的时间片完了,CPU切换到线程2执行,这个时候他就会把下一条指令也就是第十行指令的地址记录到程序计数器里面,而且这个程序计数器是属于线程1的,等线程2的代码执行完了以后,线程1再次抢到了时间片,线程1就会在自己的程序计数器里把下一行的地址取出来,继续向下运行指令。每个线程都有自己的程序计数器,因为他们各自执行的指令地址都是不一样的,所以每个线程都应该有自己的程序计数器。在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
程序计数器为什么被设定为线程私有?
为了能够准确记录各个线程正在执行的当前字节码指令地址,最好的办法就是为每
一个线程分配一个程序计数器。每个线程在创建后,都会产生自己的程序计数器和
栈帧,程序计数器在各个线程之间互不影响。
  1. 程序计数器是在Java虚拟机规范中规定的唯一一个不会存在内存溢出(OOM,OutOfMemoryError)的区。
    比如其他的一些区(堆、栈、方法区之类的)他们都会出现内存溢出。
  2. 执行java方法时,程序计数器是有值的,执行native本地方法时,程序计数器的值为空。
    任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined)。
为什么在执行native本地方法时,程序计数器的值为空(Undefined)?
因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法
相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。
由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并
且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
  1. 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。也是运行速度最快的存储区域。
  2. 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  3. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

版权声明:本文为PurineKing原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。