Liuqichun's Blog

JVM 笔记 1

JVM 学习笔记。

JVM 如何执行 Java 字节码?

HotSpot 虚拟机的编译器

基本数据类型

基本数据类型

基本数据类型在 Java 中的实现

1	...
2         5: ifeq          10
3	...
4        12: if_icmpne     17
5	...

类加载

Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(Platform Class Loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。

链接

链接可以分为验证、准备和解析三个阶段。

 1  // main 方法
 2  public static void main(String[] args) {
 3      int a = 5;
 4      boolean b = false;
 5      String c = "abc";
 6      float f = 5.0f;
 7      System.out.println(f);
 8  }
 9
10  // 反编译 main 方法代码
11  public static void main(java.lang.String[]);
12    descriptor: ([Ljava/lang/String;)V
13    flags: ACC_PUBLIC, ACC_STATIC
14    Code:
15      stack=2, locals=5, args_size=1
16         0: iconst_5
17         1: istore_1
18         2: iconst_0
19         3: istore_2
20         4: ldc           #2                  // String abc
21         6: astore_3
22         7: ldc           #3                  // float 5.0f
23         9: fstore        4
24        11: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
25        14: fload         4
26        16: invokevirtual #5                  // Method java/io/PrintStream.println:(F)V
27        19: return

初始化

初始化这个过程主要是为类的成员变量赋值。特殊的静态成员变量在类加载时进行初始化(由 JVM 完成),除此之外的(构造方法中)直接赋值操作以及静态代码块中的代码,则被 Java 编译器置于同一方法 <clinit>,最后 JVM 同步执行 <clinit> 方法。类加载的最后一步是初始化,为标记常量的字段赋值和执行 ,初始化过程中 JVM 通过加锁来确保此方法只执行一次。

类使用时必须经过初始化,那么什么时候会触发类初始化?

  1. 当虚拟机启动时,初始化用户指定的主类(main 所在的类);
  2. 当遇到创建实例的 new 指令时,初始化 new 指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  5. 子类的初始化会触发父类的初始化;
  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射 API 对某个类进行反射调用时,初始化这个类;
  8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

单例模式中,利用静态类使 Singleton 安全地初始化一个实例,其一利用静态类延迟初始化特性,其二利用 JVM 只会确保类只被加载一次。

 1public class Singleton {
 2
 3  private Singleton() {}
 4
 5  private static class LazyHolder {
 6    static final Singleton INSTANCE = new Singleton();
 7  }
 8
 9  public static Singleton getInstance() {
10    return LazyHolder.INSTANCE;
11  }
12}

重载

重载选择方法的过程:

  1. 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法;
  2. 如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法;
  3. 如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。
阶段 是否考虑字段拆装箱 是否选取可变长参数
1 否 否
2 是 否
3 是 是

多态

JVM 静态绑定和动态绑定

JVM 如何识别方法 —— 依靠类名、方法名以及方法描述符。方法描述符由方法参数类型以及返回值类型构成,如下方代码中 descriptor: ([Ljava/lang/String;)V。

在同一个类中如果出现多个名字相同且描述符也相同的方法,那么 JVM 在类验证阶段报错。

 1  // javap -c -p -v -l -constants -v Foo.class
 2  public static void main(String[] args) {
 3    int a = 5;
 4  }
 5  // main 方法字节码反编译结果
 6  public static void main(java.lang.String[]);
 7    descriptor: ([Ljava/lang/String;)V
 8    flags: ACC_PUBLIC, ACC_STATIC
 9    Code:
10      stack=1, locals=2, args_size=1
11         0: iconst_5
12         1: istore_1
13         2: return
14      LineNumberTable:
15        line 3: 0
16        line 4: 2

Java 字节码中与调用相关的指令

  1. invokestatic:用于调用静态方法。
  2. invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。
  3. invokevirtual:用于调用非私有实例方法。
  4. invokeinterface:用于调用接口方法。
  5. invokedynamic:用于调用动态方法。

对于 invokevirtual 以及 invokeinterface 而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。

调用指令的符号引用

符号引用即反编译后字节码中的 #数字 ,这些引用指向类的常量池中的值。

常量池 可以简单理解为一个存储类信息的一维数组,它在未加载到 JVM 堆中时持久存储在 class 文件中。

 1// Java 源码
 2public class Main {
 3    public static void main(String[] args) {
 4        int a = 5;
 5        int b = a;
 6    }
 7}
 8
 9// 反编译后 Main 类的常量池   javap -v Main.class
10Constant pool:
11   #1 = Methodref          #3.#21         // java/lang/Object."<init>":()V
12   #2 = Class              #22            // elltor/jvm/Main
13   #3 = Class              #23            // java/lang/Object
14   #4 = Utf8               <init>
15   #5 = Utf8               ()V
16   #6 = Utf8               Code
17   #7 = Utf8               LineNumberTable
18   #8 = Utf8               LocalVariableTable
19   #9 = Utf8               this
20  #10 = Utf8               Lelltor/jvm/Main;
21  #11 = Utf8               main
22  #12 = Utf8               ([Ljava/lang/String;)V
23  #13 = Utf8               args
24  #14 = Utf8               [Ljava/lang/String;
25  #15 = Utf8               a
26  #16 = Utf8               I
27  #17 = Utf8               b
28  #18 = Utf8               MethodParameters
29  #19 = Utf8               SourceFile
30  #20 = Utf8               Main.java
31  #21 = NameAndType        #4:#5          // "<init>":()V
32  #22 = Utf8               elltor/jvm/Main
33  #23 = Utf8               java/lang/Object

对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找。

  1. 在 C 中查找符合名字及描述符的方法。
  2. 如果没有找到,在 C 的父类中继续搜索,直至 Object 类。
  3. 如果没有找到,在 C 所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。并且,如果目标方法在间接实现的接口中,则需满足 C 与该接口之间没有其他符合条件的目标方法。如果有多个符合条件的目标方法,则任意返回其中一个。

对于接口符号引用,假定该符号引用所指向的接口为 I,则 Java 虚拟机会按照如下步骤进行查找。

  1. 在 I 中查找符合名字及描述符的方法。
  2. 如果没有找到,在 Object 类中的公有实例方法中搜索。
  3. 如果没有找到,则在 I 的超接口中搜索。这一步的搜索结果的要求与非接口符号引用步骤 3 的要求一致。

经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。

注意:Java 虚拟机识别方法的方式略有不同,除了方法名和参数类型之外,它还会考虑返回类型。

在 Java 虚拟机中,静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。由于 Java 编译器已经区分了重载的方法,因此可以认为 Java 虚拟机中不存在重载。

在 class 文件中,Java 编译器会用符号引用指代目标方法。在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。

虚方法调用

Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用都会被编译成 invokeinterface 指令。这两种指令,均属于 Java 虚拟机中的虚方法调用。

动态绑定、静态绑定

方法表

Java 虚拟机中采取了一种用空间换取时间的策略来实现动态绑定。它为每个类生成一张方法表,用以快速定位目标方法。

方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

动态绑定的实现

在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。

 1abstract class Passenger {
 2  abstract void passThroughImmigration();
 3  @Override
 4  public String toString() { ... }
 5}
 6class ForeignerPassenger extends Passenger {
 7	 @Override
 8 	void passThroughImmigration() { /* 进外国人通道 */ }
 9}
10class ChinesePassenger extends Passenger {
11  @Override
12  void passThroughImmigration() { /* 进中国人通道 */ }
13  void visitDutyFreeShops() { /* 逛免税店 */ }
14}
15
16Passenger passenger = ...
17passenger.passThroughImmigration();

image.png

<< Previous Post

|

Next Post >>

#JVM