
文章目录提示:文章思路是借鉴了宋老师的视频讲解顺序,另外对《深入理解java虚拟机》这本书也是把重点的地方进行了搬运,可以说是很全了,秉承着开源精神和极客精神写了这篇博文,希望读者能够动动尊贵的手指关注、点赞(哥,别老白嫖,总结很辛苦的,拜托了)。
首先看下《深入理解java虚拟机》对程序计数器怎么介绍的:
“程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后,能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程之间互不影响。独立存储,我们称这了内存区域为“线程私有”的内存。
如果线程正在执行的是一个java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址;如果执行的是本地方法(Native),这个计数器值则应为空(Undifined)。此内存区域是唯一一个在《java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。”
在讲运行时数据区之前,先上老图:
“java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的高强,墙外面的人想进去,墙里面的人想出来”,这也是为什么介绍运行时数据区的原因,只有搞明白了运行时数据区之后,你才能知道,我们的Class文件在经过类加载子系统的“加载→链接→初始化”之后,把文件内容拆开怎么放的,一个线程怎么运行的,jvm怎么动态管理内存的,怎么垃圾回收的等等。
宋老师在讲这部分的时候,用了一个后厨的图片形象的比喻了这一块,上图:
把整个运行时数据区比作上图,先把各种原材料准备好,该放哪的放哪,厨师就是执行引擎。
(这块只是为了理解对内存做个简单的介绍)
上图是阿里对运行时数据区的划分,《深入理解java虚拟机》把CodeCache放元空间(JDK7以及JDK之前称为方法区)中,如果看书的话,发现这块不一样,也不要觉得有问题,不要太纠结这块,毕竟运行时数据区的重点还是在栈、堆以及方法区。
每个JVM只有一个Runtime实例。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。
如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己创建的线程。
这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
程序计数器(PC寄存器,以后都叫PC寄存器了)是用来存储指向下一条指令的地址的,也就是说指向即将要执行的指令代码,存储引擎根据PC寄存器读取下一条指令。
举例:
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
String s = "abc";
System.out.println(i);
System.out.println(k);
}
}
查看字节码
看字节码的方法:https://blog.csdn.net/21aspnet/article/details/88351875
Classfile /F:/IDEAWorkSpaceSourceCode/JVMDemo/out/production/chapter04/com/atguigu/java/PCRegisterTest.class Last modified 2020-11-2; size 675 bytes MD5 checksum 53b3ef104479ec9e9b7ce5319e5881d3 Compiled from "PCRegisterTest.java" public class com.atguigu.java.PCRegisterTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#26 // java/lang/Object."":()V #2 = String #27 // abc #3 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream; #4 = Methodref #30.#31 // java/io/PrintStream.println:(I)V #5 = Class #32 // com/atguigu/java/PCRegisterTest #6 = Class #33 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/atguigu/java/PCRegisterTest; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 i #19 = Utf8 I #20 = Utf8 j #21 = Utf8 k #22 = Utf8 s #23 = Utf8 Ljava/lang/String; #24 = Utf8 SourceFile #25 = Utf8 PCRegisterTest.java #26 = NameAndType #7:#8 // " ":()V #27 = Utf8 abc #28 = Class #34 // java/lang/System #29 = NameAndType #35:#36 // out:Ljava/io/PrintStream; #30 = Class #37 // java/io/PrintStream #31 = NameAndType #38:#39 // println:(I)V #32 = Utf8 com/atguigu/java/PCRegisterTest #33 = Utf8 java/lang/Object #34 = Utf8 java/lang/System #35 = Utf8 out #36 = Utf8 Ljava/io/PrintStream; #37 = Utf8 java/io/PrintStream #38 = Utf8 println #39 = Utf8 (I)V { public com.atguigu.java.PCRegisterTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/atguigu/java/PCRegisterTest; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=5, 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: ldc #2 // String abc 12: astore 4 14: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 17: iload_1 18: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 21: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 24: iload_3 25: invokevirtual #4 // Method java/io/PrintStream.println:(I)V 28: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 line 14: 10 line 15: 14 line 16: 21 line 18: 28 LocalVariableTable: Start Length Slot Name Signature 0 29 0 args [Ljava/lang/String; 3 26 1 i I 6 23 2 j I 10 19 3 k I 14 15 4 s Ljava/lang/String; } SourceFile: "PCRegisterTest.java"
由上图就可以理解为PC寄存器存储了下一个将要执行的指令的偏移地址。
三、CPU时间片提醒读者:后面在学习运行时数据区的其他部分的时候,主要关注点就是有没有垃圾回收(GC)和有没有OOM(OutOfMemoryError)。
同步在一定程度上可以简单的理解为一种串行操作,执行到同步代码块的时候,大家都得一个一个来,带有优先级的抢占式操作(虽然说这个优先级用处也不大,只是增大了分配cpu时间片的概率)
五、通过面试题理解程序计数器
- 异步操作既可以是并行的也可以是并发的
- 其实大多数系统运行中,场景都是并行、并发同时存在的。
使用PC寄存器存储字节码指令有什么用呢?
为什么使用PC寄存器记录当前线程的执行地址呢?
我们前面也说了PC寄存器存储的是指向下一条指令的地址,后面也说了CPU把每个线程分配为一个个时间片,也提了什么是并发,异步的概念。为什么把它们放前面说是有原因的,希望你在看结论前先尝试自己解释一下嘛。下面我先空出来一部分,免得你个老六看到了结论。
。
。
。
。
。
。
。
。
。
。
。
。
。
。
。
。
我们说啊,一个系统里面在运行过程中,肯定不止一个线程,假如说我们两个异步线程,A和B。CPU肯定把它们划分为时间片呀,然后需要不断地切换各个线程,切换过来切换过去。。。CPU总得知道它切换过来之后干啥吧?(不能闲着吧?)我们说PC寄存器存储了下一个将要执行的指令,确切的说是指令的偏移地址(这下子就通透了吧),那么CPU里的执行引擎就可以根据偏移地址,找到要执行的指令,然后继续执行。我一个线程如果在执行的过程中,也要根据这个PC寄存器知道我下一步要干啥,你可以把PC寄存器就理解为一个游标。
综上:
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
为什么PC寄存器要设置为私有?
其实按照我上面的解释,你也能理解,相反地,如果大家公用一个PC寄存器的话,我CPU切换过来后,我怎么知道你这个指令不是其他线程的呢,因为前面也说了PC寄存器占用的内存很小,所以索性就线程私有了,大家一人一个,各自记录各自下面即将要执行的指令,执行引擎也好区分。
----
文章思路是借鉴了宋老师的视频讲解顺序,另外对《深入理解java虚拟机》这本书也是把重点的地方进行了搬运,可以说是很全了,秉承着开源精神和极客精神写了这篇博文,希望读者能够动动尊贵的手指关注、点赞(哥,别老白嫖,总结很辛苦的,拜托了)。