概述

当完成初始化后,就会轮到执行引擎对我们的类进行使用了,同时执行引擎还会用到运行时数据区。
JVM类加载器视图.png

运行时数据区.jpg
每个 Java 应用都有一个自己的运行实例(Runtime),可以通过这个类获取正在运行的应用的环境信息。

线程

  1. 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。
  2. 后台线程包括以下:
    1. 虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型括 “stop-the-world” 的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
    2. 周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
    3. GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
    4. 编译线程:这种线程在运行时会将字节码编译成到本地代码。
    5. 信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

      程序读数器

      这里,并非是广义上所指的物理寄存器,或许将其翻译为 PC 计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
      PC 寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令,并执行该指令。
      PC寄存器.png

      虚拟机栈

      | | 优点 | 缺点 | | —- | —- | —- | | 基于寄存器设计 | 速度快,深度优化 | 跨平台能力差 | | 基于栈设计 | 跨平台能力强,编译器容易实现 | 速度慢,实现同样的功能需要更多指令 |

由于跨平台特性,Java 的指令无法基于寄存器设计(因为与底层硬件的耦合度高,)
-Xss 设置线程的最大栈空间。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的。这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
Java方法有两种返回函数的方式。

  1. 一种是正常的函数返回,使用return指令。
  2. 另一种是方法执行中出现未捕获处理的异常,以抛出异常的方式结束。

但不管使用哪种方式,都会导致栈帧被弹出。

内部结构

每个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
  • 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
  • 一些附加信息

    局部变量表

    一个数字数组,用于存储方法参数和定义在方法体内的局部变量。数据类型包含各类的基本数据类型、对象引用(reference)以及 returnAddress 返回值类型。
    局部变量表所需的容量大小在编译期确定下来,保存在方法的 Code 属性的 maximum local variables 数据项中。

    slot

  • 32 位以内的类型占用一个 slot, 64 位的类型占用两个 slot。

  • this 指针在索引下标为 0 的位置,对于 static 方法则没有 this 。
  • 为节省内存,slot 是可以被重用的,在编译时可确认。
  • 和类变量不同,由于局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为初始化,否则无法使用。
  • 局部变量表中的变量也是重要的垃圾回收根节点(Root),只要被局部变量表中直接或间接引用的对象都不会被回收。

    操作数栈

    在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)。
  1. 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
  2. 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这时方法的操作数栈是空的。
  3. 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为 stack 的值。
  4. 栈中的任何一个元素都是可以任意的Java数据类型
    • 32bit 的类型占用一个栈单位深度
    • 64bit 的类型占用两个栈单位深度
  5. 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。只不过操作数栈是用数组这个结构来实现的而已
  6. 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
  7. 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
  8. 另外,我们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

    栈顶缓存技术

  9. 前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数(也就是你会发现指令很多)和导致内存读/写次数多,效率不高。

  10. 由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存(TOS,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
  11. 寄存器的主要优点:指令更少,执行速度快,但是指令集(也就是指令种类)很多

栈顶缓存技术实际是模板解释器的一个副产物,模板解释器下所有的字节码指令的实现都是通过指定的汇编指令实现,正是基于此才能将栈顶值显示的放到 rax 寄存器中,且需要频繁读取栈顶值的字节码指令可以显示的读取 rax 寄存器中内容。

动态链接(指向运行时常量池的方法引用

当编译一个 Java 程序时,会得到程序中每个类或接口的独立的 Class 文件。
当程序进行时,JVM 通过类加载器加载相关的类和接口,在动态连接的过程中把它们相互串联起来。

  • 常量池:每个被 JVM 半其的类或接口的内部都有一份自己的常量池。
  • 运行时常量池:常量池中的符号引用被解析后放入运行时常量池。当一个类被首次装载后,所有来自该类的符号引用都会写入该类的运行时常量池(位于方法区中)。
  • 经过解析操作后,将符号引用替换为直接引用,解析的过程会查询全局字符串池(StringTable),以保证运行时常量池所引用的字符串与全局字符串所引用的是一致的。
  1. 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic 指令
  2. 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。 ```java Constant pool:

    7 = Methodref #8.#31 // com/atguigu/java1/DynamicLinkingTest.methodA:()V

    8 = Class #32 // com/atguigu/java1/DynamicLinkingTest

    13 = Utf8 ()V

    19 = Utf8 methodA

    31 = NameAndType #19:#13 // methodA:()V

    32 = Utf8 com/atguigu/java1/DynamicLinkingTest

    9: invokevirtual #7 // Method methodA:()V
  1. 通过 `#7` 我们就能找到需要调用的 `methodA()` 方法,并进行调用。<br />为什么要用常量池 ?
  2. 1. 因为在不同的方法,都可能调用常量或方法,所以只需要存储一份字符串数据即可,并记录其常量的偏移量 `#1` 即可,节省空间。
  3. <a name="SyAbR"></a>
  4. # 方法调用
  5. JVM 中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
  6. | 链接类型 | 描述 |
  7. | --- | --- |
  8. | 静态链接 | 当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在**编译期确定**,且**运行期保持不变**时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。 |
  9. | 动态链接 | 如果被调用的方法在**编译期无法被确定**下来,也就是说,只能够在程序**运行期**将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。 |
  10. 静态链接与动态链接针对的是方法。早期绑定和晚期绑定范围更广。早期绑定涵盖了静态链接,晚期绑定涵盖了动态链接。
  11. | 绑定类型 | 描述 |
  12. | --- | --- |
  13. | 早期绑定 | 早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。<br />对于 finalstaticprivate和构造方法是前期绑定。 |
  14. | 后期绑定 | 可在运行期间判断对象的类型,并分别调用适当的方法。编译器编译阶段无法确定对象类型,但方法调用机制能让执行引擎在运行时自己去查找和获取正确的方法主体。 |
  15. 动态绑定的过程:
  16. 1. 虚拟机提取对象的实际类型的方法表
  17. 1. 虚拟机搜索方法签名
  18. 1. 调用方法
  19. 对于 static 方法,具体的原理我也说不太清。不过根据网上的资料和我自己做的实验可以得出结论:static 方法可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。(这里意思是说如果父类里有一个 static 方法,它的子类里如果没有对应的方法,那么当子类对象调用这个方法时就会使用父类中的方法。而如果子类中定义了相同的方法,则会调用子类的中定义的方法。唯一的不同就是,当子类对象上转型为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法。因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法)由上面我们可以得出结论,如果一个方法不可被继承或者继承后不可被覆盖,那么这个方法就采用的静态绑定。
  20. <a name="ZBpmw"></a>
  21. # 多态与绑定
  22. 封装、继承和多态是面向对象的三大特性。Java 中任何一个普通的方法其实都具备虚函数的特征。如果在 Java 程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字 final 来标记这个方法。
  23. <a name="ZAm3T"></a>
  24. ## 虚方法与非虚方法
  25. <a name="QdbGz"></a>
  26. ### 虚方法与非虚方法的区别
  27. 1. 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
  28. 1. 静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法。
  29. 1. 其他方法称为虚方法。
  30. <a name="DF4Ad"></a>
  31. ### 子类对象的多态的使用前提:
  32. 1. 类的继承关系
  33. 1. **方法重写**
  34. <a name="bHq3B"></a>
  35. ### 虚拟机中调用方法的指令
  36. - 普通指令:
  37. - invokestatic:调用静态方法,解析阶段确定唯一方法版本
  38. - invokespecial:调用 `<init>` 方法、私有及父类方法,解析阶段确定唯一方法版本
  39. - invokevirtual:调用所有虚方法
  40. - invokeinterface:调用接口方法
  41. - 动态调用指令
  42. - invokedynamic:**动态解析**出需要调用的方法,然后执行
  43. 前四条指令固化在虚拟机内部,方法的调用执行不可人为干预。而 invokedynamic 指令则支持由用户确定方法版本。其中 invokestatic 指令和 invokespecial 指令调用的方法称为非虚方法,其余的(final 修饰的除外)称为虚方法。
  44. ```java
  45. class Father {
  46. public Father() {
  47. System.out.println("father的构造器");
  48. }
  49. // 静态方法:编译期确定
  50. public static void showStatic(String str) {
  51. System.out.println("father " + str);
  52. }
  53. // final方法:编译期确定
  54. public final void showFinal() {
  55. System.out.println("father show final");
  56. }
  57. // 虚方法
  58. public void showCommon() {
  59. System.out.println("father 普通方法");
  60. }
  61. }
  62. public class Son extends Father {
  63. public Son() {
  64. // invokespecial
  65. super();
  66. }
  67. public Son(int age) {
  68. // invokespecial
  69. this();
  70. }
  71. // 不是重写的父类的静态方法,因为静态方法不能被重写!
  72. public static void showStatic(String str) {
  73. System.out.println("son " + str);
  74. }
  75. private void showPrivate(String str) {
  76. System.out.println("son private" + str);
  77. }
  78. public void show() {
  79. // 非虚方法测试-----
  80. // invokestatic
  81. showStatic("atguigu.com");
  82. // invokestatic
  83. super.showStatic("good!");
  84. // invokespecial
  85. showPrivate("hello!");
  86. // invokespecial
  87. super.showCommon();
  88. // invokevirtual
  89. showFinal(); // 因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
  90. // 虚方法测试-----
  91. // invokevirtual 你没有显示的加「super.」,
  92. // 编译器认为你可能调用子类的showCommon(即使son子类没有重写,也会这么认为的),
  93. // 所以编译期间确定不下来,就是虚方法。
  94. showCommon();
  95. info();
  96. MethodInterface in = null;
  97. // invokeinterface
  98. in.methodA();
  99. }
  100. public void info() {
  101. }
  102. public void display(Father f) {
  103. f.showCommon();
  104. }
  105. public static void main(String[] args) {
  106. Son so = new Son();
  107. so.show();
  108. }
  109. }
  110. interface MethodInterface {
  111. void methodA();
  112. }

invokedynamic

  1. JVM 字节码指令集一直比较稳定,一直到 Java7 中才增加了一个 invokedynamic 指令,这是 Java 为了实现【动态类型语言】支持而做的一种改进。
  2. 但是在 Java7 中并没有提供直接生成 invokedynamic 指令的方法,需要借助 ASM 这种底层字节码工具来产生 invokedynamic 指令。直到 Java8 的 Lambda 表达式的出现,invokedynamic 指令的生成,在 Java 中才有了直接的生成方式。
  3. Java7 中增加的动态语言类型支持的本质是对 Java 虚拟机规范的修改,而不是对 Java 语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在 Java 平台的动态语言的编译器。

    动态语言和静态语言

  4. 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。

  5. 说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

    Java 语言中方法重写的本质(动态分派)

  6. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作 C。

  7. 如果在类型 C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验。
    1. 如果通过则返回这个方法的直接引用,查找过程结束
    2. 如果不通过,则返回 java.lang.IllegalAccessError 异常
  8. 否则,按照继承关系从下往上依次对 C 的各个父类进行第 2 步的搜索和验证过程。
  9. 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常。

    虚方法表(Virtual Method Table)

    在面向对象的编程中,会很频繁的使用到动态分派
作用 用来加速动态分派过程中需要在类的方法元数据中搜索合适的目标。相当于建立一个缓存索引,利用索引表代替查找。
缓存哪些数据? 每个类中都有一个虚方法表,表中存放着各个方法的实际入口
创建时机 虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM 会把该类的虚方法表也初始化完毕。

比如有以下继承关系:
【第三章】运行时数据区 - 图4
Son 在调用 toString() 方法的时候,Son 没有重写过,Son 的父类 Father 也没有重写过,那就直接调用 Object#toString() 方法。那么 Son 就直接在虚方法表里指明 toString() 直接指向 Object 类。
下次 Son 对象再调用 toString() 就直接去找 Object,就不需要再次从底向上定位方法绑定的类。

方法返回地址

本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

异常退出

方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码

Exception         table:
from    to        target  type
4        16        19        any
19        21        19        any
类型 描述
from 字节码指令起始地址
to 字节码指令结束地址
target 出现异常跳转的目标地址
type 描述异常的类型

栈相关面试题

栈的大小分为固定大小和动态大小,如果是固定的可能出现 SOF,如果是动态变化的可能会出现 OOM。
垃圾回收不会涉及到虚拟机栈。

方法中定义的局部变量是否线程安全

具体问题具体分析。