第4章-程序计数器
以下内容是由尚硅谷的宋红康老师整理,在此先感谢尚硅谷的教学课程;在此仅作为个人学学习笔记进行分享;视频教程地址:宋红康JVM:https://www.bilibili.com/video/BV1PJ411n7xZ

1-网络知识总结
1、PC 寄存器概述
文档网址
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
PC 寄存器介绍
- JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。
- 这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
- 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
- 在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefned)。
- 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何OutofMemoryError情况的区域。
PC 寄存器的作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。
2、代码示例
- 代码
java
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);
}
}
-
反编译:javap -v xxx.class
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;123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
-
左边的数字代表指令地址(指令偏移),即 PC 寄存器中可能存储的值,然后执行引擎读取 PC 寄存器中的值,并执行该指令
3、两个面试题
使用PC寄存器存储字节码指令地址有什么用呢?
或者问
为什么使用 PC 寄存器来记录当前线程的执行地址呢?
- 因为线程是一个个的顺序执行流,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
PC寄存器为什么被设定为私有的?
- 我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
- 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
- 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
- 这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
4、CPU 时间片
- CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片。
- 在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
- 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
2-个人知识总结
1-当我谈程序计数器
【指令1,指令2,指令3,指令4,指令5,指令6,指令7】->【程序计数器】存放的是当前线程指定的索引index(类比游标)

2-概念对比
1-CPU寄存器 VS JVM中Program Counter Register
1)CPU寄存器->可以理解为【硬件层面的一个存储->存储当前CPU指定的内容】
2)PC Register->可以理解为【软件层面的一个存储->存储当前线程指定的内容】
1-CPU寄存器 VS JVM中PC Register
CPU寄存器(CPU Registers)和JVM中的程序计数器(Program Counter Register)虽然在名字上有相似之处,但在计算机体系结构和JVM中的角色上有一些显著的区别。
-
CPU寄存器:
- 定义和作用:CPU寄存器是位于CPU内部的一组高速存储器单元,用于暂时存放指令、数据或地址。它们是处理器内部的物理部件,用于高速缓存和操作数据。
- 功能:CPU寄存器直接支持CPU的指令执行,包括算术运算、逻辑运算、数据移动等。寄存器的内容可以快速读取和修改,对CPU执行指令速度有重要影响。
-
JVM中的程序计数器:
- 定义和作用:程序计数器是JVM中一块较小的内存区域,用于存储当前线程正在执行的字节码指令的地址或行号。每个线程都有自己的程序计数器,它是线程私有的。
- 功能:程序计数器指示了线程执行的位置,当线程执行Java方法时,程序计数器记录正在执行的指令位置。在线程切换时,程序计数器的值被保存和恢复,确保线程可以正确继续执行。
-
主要区别:
- 位置:CPU寄存器是位于CPU内部的硬件组件,而程序计数器是JVM运行时数据区的一部分,位于每个线程的私有数据区域。
- 作用:CPU寄存器直接支持指令级操作和数据处理,而程序计数器主要用于支持Java字节码的执行和线程的控制流程。
- 大小和数量:CPU寄存器数量有限且大小固定(通常几十个到几百个字节),而程序计数器大小通常为一个机器字(例如32位或64位),数量与活跃线程数相关。
总结来说,CPU寄存器是物理CPU内部的硬件组件,用于直接支持指令执行和数据操作;而JVM中的程序计数器是一种逻辑概念,用于指示线程当前执行的字节码指令位置,是实现Java线程控制和字节码执行的关键组成部分。
2-CPU寄存器 VS JVM中PC Register
CPU寄存器与JVM中的程序计数器(Program Counter Register)虽然都属于寄存器类别,但它们在功能和应用场景上有明显的区别:
1-CPU寄存器
-
功能:CPU寄存器是中央处理器内部的一组高速存储单元,它们速度极快,用于暂存CPU在执行指令过程中需要频繁访问的数据和控制信息。CPU寄存器种类多样,包括但不限于通用寄存器、状态寄存器、指令指针寄存器等。其中,指令指针寄存器(Instruction Pointer, IP)存储下一条指令的地址,类似于JVM中的程序计数器。
-
作用:提高执行效率,减少访问主存的次数。CPU在执行指令时,会先从寄存器中读取数据,如果所需数据不在寄存器中,则会从缓存或内存中加载。寄存器的数量有限,但每个都能在单个时钟周期内访问。
-
特点:速度快、容量小、成本高、数量有限。
2-JVM中的Program Counter Register(程序计数器)
-
功能:JVM的程序计数器是每个线程私有的内存区域,它用来存储当前线程所执行的字节码指令的地址。简而言之,它记录了当前线程执行到的虚拟机字节码指令的位置,以实现方法调用、返回和异常处理等流程控制。
-
作用:线程切换时能立即恢复到正确的执行位置,管理方法调用栈,支持多线程的并发执行。它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
-
特点:
- 线程私有:每个线程都有自己的程序计数器,互不影响。
- 内存小:相比其他运行时数据区,程序计数器占用的内存空间非常小。
- 不会发生内存溢出:因为其存储的信息仅与当前线程的执行状态有关,且其大小固定或按需动态扩展,因此理论上不会出现内存不足的情况。
总结来说,CPU寄存器是硬件层面的概念,是CPU内部用于快速访问数据和控制指令的关键部件;而JVM中的程序计数器则是软件层面的概念,是JVM为了管理程序执行流程而设计的一种抽象,专用于追踪当前线程的执行位置。两者虽然都涉及到指令地址的存储,但应用环境和具体作用有本质的不同。
2-串行/并行/并发
1-串行/并行/并发 概念对比,给出现实中的例子进行说明
在计算机科学中,串行、并行和并发是描述程序执行方式的重要概念,它们在现实中有明显的区别和应用场景。
-
串行(Serial):
- 定义:串行执行是指任务按顺序一个接一个地执行,每个任务在前一个任务完成之后开始。在串行执行中,同一时间只有一个任务处于运行状态。
- 例子:单核处理器执行单线程任务时是串行的典型例子。例如,一个人在做菜时,依次处理每道菜的每个步骤,不能同时在不同的炉灶上烹饪不同的菜。
-
并行(Parallel):
- 定义:并行执行是指多个任务同时进行,每个任务在不同的处理单元(如多个CPU核心或多台计算机)上执行,各自独立进行计算。
- 例子:多核处理器同时执行多个线程或进程是并行的典型场景。例如,一个团队在准备一场音乐会时,不同乐器手在同时进行彼此独立的排练和准备工作。
-
并发(Concurrent):
- 定义:并发执行是指多个任务交替进行,看似同时执行,但实际上是通过操作系统或应用程序的调度算法,在时间上交错执行的。
- 例子:多线程程序是并发的典型例子。例如,一个在线聊天应用中,多个用户可以同时发送和接收消息,这些操作通过并发处理,但每个用户的消息处理是交错进行的,由系统调度决定。
实际例子对比:
-
串行:制造流水线上的组装工作,每个工人按照固定顺序完成各自的任务,如汽车组装中的焊接、涂漆、组装。
-
并行:在科学计算中,多个处理器同时计算一个大型模拟的不同部分,如天气预报模型中,多个处理器同时处理不同地区的数据以加速预测的生成。
-
并发:Web服务器同时处理多个用户的请求,每个请求独立运行,但通过调度算法控制,确保每个用户都能得到及时响应,比如在线购物网站同时处理多个用户的浏览和购买请求。
总结来说,串行、并行和并发是描述计算机任务执行方式的不同视角,理解它们有助于优化程序设计和提高计算效率,适应不同的应用需求和硬件环境。
2-串行/并行/并发 概念对比,给出现实中的例子进行说明
串行、并行、并发是计算机科学中描述任务执行方式的三个重要概念,它们各有特点,并在不同的应用场景中发挥着作用。下面通过现实中的例子来对比这三个概念:
1-串行
定义:串行指的是任务按照顺序一个接一个地执行,只有前一个任务完成后,下一个任务才能开始。如同排队买票,每个人必须等待前一个人买完票后才能上前购买。
现实例子:在超市结账,假设只有一个收银台开放,顾客们必须依次排队等待,前一个顾客完成结账后,下一个顾客才能开始。这种情况下,任务(结账)是以串行的方式进行的。
2-并行
定义:并行意味着多个任务能在同一时刻同时执行,它们之间没有依赖关系或通过某种机制(如同步锁)管理相互之间的依赖。这需要多核处理器或多台计算机共同工作。
现实例子:在厨房里,两位厨师同时烹饪不同的菜品,一位负责炒菜,另一位负责蒸鱼,他们各自独立工作,不需要等待对方完成,这就是并行。每位厨师的工作可以看作是一个独立的任务,他们在同一时间内并行进行。
3-并发
定义:并发强调的是多个任务在一段时间内交替执行的能力,而不是严格意义上的同时执行。系统通过快速切换任务,给人一种"同时进行"的错觉,但实际上同一时刻只有一个任务在执行。并发需要的资源可以是一个CPU核心,通过时间片轮转等调度策略来实现。
现实例子:客服中心的接线员处理来电就是一个并发的例子。尽管接线员可能一次只能接听一个电话,但在一天中,他/她能够交替处理多个来电请求,使得多个客户的问题看起来像是同时被解决。每个电话咨询就是一个任务,接线员通过快速在不同通话间切换,实现了并发处理。
4-总结
- 串行是顺序执行,一个接一个;
- 并行是真正的同时执行,需要多处理器或多核心;
- 并发则是通过时间分片或任务切换达到同时处理多个任务的效果,但实际可能并非完全同时执行。
3-OOM和垃圾回收
1)PCRegister没有【OOM】和【垃圾回收】
2)Java虚拟机Stack没有【垃圾回收】
