PC Register(程序计数器)

程序计数器是一块内存较小的空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个指示器来完成。

每个线程执行是CPU轮流切换的执行的,在任意时刻一个cup只会执行一个线程中的指令,因此为了保证线程切换后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各线程之间计数器互不影响,独立存储。我们称此类内存区域为”线程私有的”内存。

如果正在执行的是一个Java方法,那么程序计数器中记录的值是正在执行虚拟机字节码指令的地址(偏移地址)。如果执行的是一个本地方法(Native),这个计数器的值应该为空(undefined)。此内存区域是唯一个在《Java虚拟机规范》中没有规定任何发生OutOfMemoryError情况的区域。

  1. public class PCRegisterTest {
  2. public static void main(String[] args) {
  3. int a = 10;
  4. int b = 20;
  5. int c = a + b;
  6. }
  7. }
  8. /**反编译后的字节码文件*/
  9. Last modified 2020-10-23; size 487 bytes
  10. MD5 checksum 31251024dab7a4ec72de4f01c26f4c30
  11. Compiled from "PCRegisterTest.java"
  12. public class sprit.vm.runtime.PCRegisterTest
  13. minor version: 0
  14. major version: 52
  15. flags: ACC_PUBLIC, ACC_SUPER
  16. Constant pool:
  17. #1 = Methodref #3.#21 // java/lang/Object."<init>":()V
  18. #2 = Class #22 // sprit/vm/runtime/PCRegisterTest
  19. #3 = Class #23 // java/lang/Object
  20. #4 = Utf8 <init>
  21. #5 = Utf8 ()V
  22. #6 = Utf8 Code
  23. #7 = Utf8 LineNumberTable
  24. #8 = Utf8 LocalVariableTable
  25. #9 = Utf8 this
  26. #10 = Utf8 Lsprit/vm/runtime/PCRegisterTest;
  27. #11 = Utf8 main
  28. #12 = Utf8 ([Ljava/lang/String;)V
  29. #13 = Utf8 args
  30. #14 = Utf8 [Ljava/lang/String;
  31. #15 = Utf8 a
  32. #16 = Utf8 I
  33. #17 = Utf8 b
  34. #18 = Utf8 c
  35. #19 = Utf8 SourceFile
  36. #20 = Utf8 PCRegisterTest.java
  37. #21 = NameAndType #4:#5 // "<init>":()V
  38. #22 = Utf8 sprit/vm/runtime/PCRegisterTest
  39. #23 = Utf8 java/lang/Object
  40. {
  41. public sprit.vm.runtime.PCRegisterTest();
  42. descriptor: ()V
  43. flags: ACC_PUBLIC
  44. Code:
  45. stack=1, locals=1, args_size=1
  46. 0: aload_0
  47. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  48. 4: return
  49. LineNumberTable:
  50. line 3: 0
  51. LocalVariableTable:
  52. Start Length Slot Name Signature
  53. 0 5 0 this Lsprit/vm/runtime/PCRegisterTest;
  54. public static void main(java.lang.String[]);
  55. descriptor: ([Ljava/lang/String;)V
  56. flags: ACC_PUBLIC, ACC_STATIC
  57. Code:
  58. stack=2, locals=4, args_size=1
  59. 0: bipush 10 //入栈一个int 类型的常量10
  60. 2: istore_1 //将一个数值从操作数栈存储到局部变量表
  61. 3: bipush 20
  62. 5: istore_2
  63. 6: iload_1
  64. 7: iload_2
  65. 8: iadd
  66. 9: istore_3
  67. 10: return
  68. LineNumberTable:
  69. line 6: 0
  70. line 7: 3
  71. line 8: 6
  72. line 9: 10
  73. LocalVariableTable:
  74. Start Length Slot Name Signature
  75. 0 11 0 args [Ljava/lang/String;
  76. 3 8 1 a I
  77. 6 5 2 b I
  78. 10 1 3 c I
  79. }
  80. SourceFile: "PCRegisterTest.java"

pcregister.png

Tips: 为什么字节码指令不是连续的? 由于跨平台性设计,Java的指令都是基于栈式的指令集架构设计的,不同平台CPU架构不同,所以不能设计为寄存器架构。栈式架构的优点是:跨平台、指令集小、编译器容易实现。缺点是:性能下降,实现相同的功能需要更多的指令。 局部变量表就是通过 esp ebp寻址的所以 bipush 10 可能就解释成 mov eax 10 mov esp-4 eax 两条指令 https://guoqianliang.blog.csdn.net/article/details/107569880

虚拟机栈

运行时数据区 - 图2
与程序计数器一样,Java虚拟机栈也是线程私有的内存区域,与线程的生命周期相同。虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)。用于存储局部变量表,操作数栈,方法返回地址,动态链接和其他信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

在活动线程中只有处于最顶上的栈帧是有效的,称为当前栈帧,正在执行的方法称之为当前方法。在执行引擎运行时,所有指令只能针对当前栈帧进行操作。

虚拟机栈通过pop和push的操作,对每个方法的对应的活动栈帧进行运算,方法正常结束,肯定会跳到下一个栈帧上。在执行过程中,如果发生异常,会进行异常回溯,返回地址通过异常表确定。

异常信息

  • StackOverflowError: 常见于递归、迭代。
  • OutOfMemoryError: HotSpot虚拟机不可以动态扩展虚拟机栈容量。

局部变量表(LocalVariablesTable)

存放了编译期可知的各种Java虚拟机基本数据类型、对象引用和returnAddress。
这些数据类型在布局变量中的存储空间以局部变量槽Slot)来表示,其中64位长度的long和double类型的数据会占据两个变量槽,其余数据类型只占据一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量表空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
运行时数据区 - 图3
如上图所示:该方法的局部变量表占据4个槽。

操作数栈(OenrandStack)

通过名字就知道是一个栈结构。Java虚拟机的解释执行引擎被称为基于栈的执行引擎,其中的栈指的就是操作数栈。当JVM为方法创建栈帧的时候,在栈帧内部创建一个操作数栈,保证方法内部指令可以完成工作。

  1. public class OperandStackTest {
  2. public int sum(int a,int b){
  3. int c = a + b;
  4. return c;
  5. }
  6. }

反编译OperandStackTest.class

  1. javap -v OpenrandStackTest.class
  1. public int sum(int, int);
  2. descriptor: (II)I
  3. flags: ACC_PUBLIC
  4. Code:
  5. stack=2, locals=4, args_size=3 // 最大栈深度为2 局部变量个数为4个
  6. 0: iload_1 // 局部变量1压栈
  7. 1: iload_2 // 局部变量2压栈
  8. 2: iadd // 取出两个局部变量计算压栈
  9. 3: istore_3 // 从操作数栈放入局部变量3
  10. 4: iload_3 // 局部变量3压栈
  11. 5: ireturn
  12. LineNumberTable:
  13. line 7: 0
  14. line 8: 4

动态链接

每一个栈帧中都包含一个常量池对当前方法的引用,目的是支持目的是支持方法调用过程的动态连接

方法返回地址

方法执行时退出有两种情况

  • 正常退出,正常执行到方法的退出指令,如 RETURN、IRETURN、ARETURN
  • 异常退出

正常退出 使用当前栈帧来恢复调用者的状态,包括它的局部变量表和操作数栈,调用者的程序计数器适当递增来跳过方法调用指令,然后在调用方法的栈帧继续运行,如何有返回值则将返回值压入当前栈帧的操作数栈。

异常退出 方法执行过程中发生异常,如果该方法没有处理异常,那么该方法会突然结束。突然结束的方法调用永远不向其调用者返回值,异常信息抛给能够处理的栈帧。

本地方法栈

与虚拟机栈发挥的作用及其相似,其区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈是为虚拟机执行本地方法所服务。和虚拟机栈一样,本地方法栈也会在栈深度溢出和栈扩展失败时抛出StackOverflowError和OutOfMemoryError。