JVM程序计数器的作用与特点
定义
上图为程序计数器在JVM中的位置,它属于JVM内存的一部分,Program Counter Register 程序计数器(寄存器)
程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
作用
先看下面的代码
package org.slumberjax.jvm.d02;
import java.io.PrintStream;
public class T01 {
public static void main(String[] args) {
PrintStream out = System.out;
out.println(1);
out.println(2);
out.println(3);
out.println(4);
out.println(5);
}
}
使用反汇编工具对该类字节码进行反汇编,命令如下:
javap -c T01.class
得到如下内容:
Compiled from "T01.java"
public class org.slumberjax.jvm.d02.T01 {
public org.slumberjax.jvm.d02.T01();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: astore_1
4: aload_1
5: iconst_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
9: aload_1
10: iconst_2
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: aload_1
15: iconst_3
16: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
19: aload_1
20: iconst_4
21: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
24: aload_1
25: iconst_5
26: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
29: return
}
- 其中Code代表字节码指定的偏移地址,在运行时为实际地址而不是这里的数字
- 偏移地址对应的iconst、getstatic等等是jvm 中的操作指令
- CPU无法对JVM操作指令直接执行,编译后的字节码在没有经过JIT(实时编译器)编译前,是通过字节码解释器进行解释执行。其执行原理为:字节码解释器读取内存中的字节码,按照顺序读取字节码指令,读取一个指令就将其翻译成固定的操作,根据这些操作进行分支,循环,跳转等动作。
以上面的反汇编后的结果(main方法)为例进行说明:
- jvm拿到第一条Code 0对应的getstatic指令交给解释器解释为机器码再交给CPU执行,同时将下一条指令的偏移地址Code 3放入程序计数器
- 第一条Code 0对应指令执行完毕后,解释器在程序计数器中取出下一条指令的偏移地址Code 3,获取到对应的jvm指令astore_1交给解释器解释为机器码后交给CPU执行,同时将下一条指令的偏移地址Code 4放入程序计数器
- 重复上述步骤完成程序的执行
总结: 程序计数器是一个记录着当前线程将要执行的下一条jvm指令地址的指示器
特点
从上面的内容中并不能体现出程序计数器的应用场景,在单线程情况下,程序计数器的存在毫无意义(完全可以按顺序执行完所有jvm指令而无需程序计数器),下图为多线程情况下对前文代码的执行流程:
在多线程下,CPU调度时会为每个线程分配时间片,若线程在时间片内代码未执行完毕将线程的状态暂存并进行线程切换,而程序计数器在线程切换时记录下一条指令的地址
- thread1执行完1(0:getstatic)和(3:astore_1)后CPU切换到thread2,此时thread1的程序计数器记录下一条指令的地址4
- thread2执行完1(0:getstatic)和(3:astore_1)后CPU切回到thread1,从thread1的程序计数器中取出下一条指令要执行的地址4对应的指令aload_1进行执行,执行到5(5:iconst_1)时,CPU切换到thread2,此时thread1的程序计数器记录下一条指令的地址6...
- 后面不在赘述...
由此可以得知程序计数器一个最重要的特点: 线程隔离性,每个线程工作时都有属于自己的独立计数器。
总结:
- 线程隔离性,每个线程工作时都有属于自己的独立计数器。
- 执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址(参考上一小节的描述)。
- 执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
- 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
- 程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域(不会内存溢出)。