提示:文章思路是借鉴了宋老师的视频讲解顺序,另外对《深入理解java虚拟机》这本书也是把重点的地方进行了搬运,可以说是很全了,秉承着开源精神和极客精神写了这篇博文,希望读者能够动动尊贵的手指关注、点赞(哥,别老白嫖,总结很辛苦的,拜托了)。
文章目录
前言
首先看下《深入理解java虚拟机》对程序计数器怎么介绍的:
“程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后,能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各个线程之间互不影响。独立存储,我们称这了内存区域为“线程私有”的内存。
如果线程正在执行的是一个java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址;如果执行的是本地方法(Native),这个计数器值则应为空(Undifined)。此内存区域是唯一一个在《java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。”
一、背景知识
在讲运行时数据区之前,先上老图:
“java与C++之间有一堵有内存动态分配和垃圾收集技术所围成的高强,墙外面的人想进去,墙里面的人想出来”,这也是为什么介绍运行时数据区的原因,只有搞明白了运行时数据区之后,你才能知道,我们的Class文件在经过类加载子系统的“加载→链接→初始化”之后,把文件内容拆开怎么放的,一个线程怎么运行的,jvm怎么动态管理内存的,怎么垃圾回收的等等。
宋老师在讲这部分的时候,用了一个后厨的图片形象的比喻了这一块,上图:
把整个运行时数据区比作上图,先把各种原材料准备好,该放哪的放哪,厨师就是执行引擎。
1.1 何为内存
(这块只是为了理解对内存做个简单的介绍)
- 内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局。
上图是阿里对运行时数据区的划分,《深入理解java虚拟机》把CodeCache放元空间(JDK7以及JDK之前称为方法区)中,如果看书的话,发现这块不一样,也不要觉得有问题,不要太纠结这块,毕竟运行时数据区的重点还是在栈、堆以及方法区。
- 我们通过磁盘或者网络IO得到的数据,都需要先加载到内存中,然后CPU从内存中获取数据进行读取,也就是说内存充当了CPU和磁盘之间的桥梁。
1.2 Runtime类
每个JVM只有一个Runtime实例。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。
1.3 何为线程?
- 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
- 在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射
- 当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。Java线程执行终止后,本地线程也会回收。
- 操作系统负责将线程安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法。
1.4 JVM 系统线程
-
如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己创建的线程。
-
这些主要的后台系统线程在Hotspot JVM里主要是以下几个:
- 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型括”stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
- 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行
-GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持 - 编译线程:这种线程在运行时会将字节码编译成到本地代码
- 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理
二、程序计数器(PC寄存器)
2.1 概念
- JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
- 这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
2.2 作用
程序计数器(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.youkuaiyun.com/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."<init>":()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 <init>
#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 // "<init>":()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."<init>":()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寄存器存储了下一个将要执行的指令的偏移地址。
2.3 介绍
- 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java- 方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned)(本地方法是C写的,java咋记录嘛)。
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
提醒读者:后面在学习运行时数据区的其他部分的时候,主要关注点就是有没有垃圾回收(GC)和有没有OOM(OutOfMemoryError)。
三、CPU时间片
- CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
- 在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
- 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
四、并行、并发、串行
- 串行就是各个线程排队类,线程C执行完,再执行线程B,线程B执行完再执行A。。。。多是针对单cpu单核来说,
同步在一定程度上可以简单的理解为一种串行操作,执行到同步代码块的时候,大家都得一个一个来,带有优先级的抢占式操作(虽然说这个优先级用处也不大,只是增大了分配cpu时间片的概率)
- 并行呢,就是大家伙一块执行,大多数是多CPU多核的系统,我财大气粗内核够用,大家一人一个,也不争抢。
- 并发呢,我的理解是一种宏观上的并行操作,看似大家一块执行的,但是微观上一个时间段内之后一个线程在执行。
- 异步操作既可以是并行的也可以是并发的
- 其实大多数系统运行中,场景都是并行、并发同时存在的。
五、通过面试题理解程序计数器
使用PC寄存器存储字节码指令有什么用呢?
为什么使用PC寄存器记录当前线程的执行地址呢?
我们前面也说了PC寄存器存储的是指向下一条指令的地址,后面也说了CPU把每个线程分配为一个个时间片,也提了什么是并发,异步的概念。为什么把它们放前面说是有原因的,希望你在看结论前先尝试自己解释一下嘛。下面我先空出来一部分,免得你个老六看到了结论。
。
。
。
。
。
。
。
。
。
。
。
。
。
。
。
。
我们说啊,一个系统里面在运行过程中,肯定不止一个线程,假如说我们两个异步线程,A和B。CPU肯定把它们划分为时间片呀,然后需要不断地切换各个线程,切换过来切换过去。。。CPU总得知道它切换过来之后干啥吧?(不能闲着吧?)我们说PC寄存器存储了下一个将要执行的指令,确切的说是指令的偏移地址(这下子就通透了吧),那么CPU里的执行引擎就可以根据偏移地址,找到要执行的指令,然后继续执行。我一个线程如果在执行的过程中,也要根据这个PC寄存器知道我下一步要干啥,你可以把PC寄存器就理解为一个游标。
综上:
- 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
为什么PC寄存器要设置为私有?
其实按照我上面的解释,你也能理解,相反地,如果大家公用一个PC寄存器的话,我CPU切换过来后,我怎么知道你这个指令不是其他线程的呢,因为前面也说了PC寄存器占用的内存很小,所以索性就线程私有了,大家一人一个,各自记录各自下面即将要执行的指令,执行引擎也好区分。
----
文章思路是借鉴了宋老师的视频讲解顺序,另外对《深入理解java虚拟机》这本书也是把重点的地方进行了搬运,可以说是很全了,秉承着开源精神和极客精神写了这篇博文,希望读者能够动动尊贵的手指关注、点赞(哥,别老白嫖,总结很辛苦的,拜托了)。