类的加载、链接和初始化(基于Java 1.8)

类的加载、链接和初始化(基于Java 1.8)

Java的数据类型(Data type)主要是有两种:

  1. 基本类型 primitive types
  2. 引用类型 reference types

其中引用类型又被细分为:

  1. 类 class types
  2. 数组 array types
  3. 接口 interface types

基本类型和数组类型是由Java虚拟机直接生成的,其他(类 class types、接口 interface types)则需要Java虚拟机对其进行链接和初始化。

1. Java虚拟机启动 Java Virtual Machine Startup

  • 通过引导类加载器创建一个初始类来启动,并执行这个public class中的void main(String[])方法。
  • 初始类可以作为命令行参数提供。或者,该实现可以提供一个初始类,该初始类设置一个类加载器,该类加载器进而加载。

2. 创建和加载 Creation and Loading

  • 指查找字节流,并据此创建类的过程。
  • 其中数组(array types)是没有字节流的,由Java虚拟机直接生成,对于其他的类来说,Java虚拟机需要借助于类加载器来完成查找字节流的工程。

2.1 类加载器 ClassLoader

在Java虚拟机规范中,类加载器被分为两种:

  1. Java虚拟机提供的引导类加载器(bootstrap class loader)
  2. 用户定义的类加载器(user-defind class loaders)

2.1.1 Java虚拟机提供的引导类加载器 bootstrap class loader

  • bootstrap class loader 由Java虚拟机提供的
  • 这个类加载器是使用C++实现的,没有对应的Java对象。

2.1.2 用户定义的类加载器 user-defind class loaders

  • user-defind class loaders 是Java虚拟机规范中对于类加载器的分类划分,是一个统称,实际上并没有这个类加载器
  • 用户定义的类加载器都是java.lang.ClassLoader类的子类
  • 在Java虚拟机规范中提到,用户定义的类加载器可以实现通过网络下载类,动态生成类或从加密文件中提取类
  • 用户定义的类加载器需要由bootstrap class loader去加载
  • 在Java1.8的核心类库中,提供了两个类加载器,分别是:
    1. 扩展类加载器 extention class loader
      sun.misc.Launcher.ExtClassLoader
    2. 应用类加载器 application class loader
      sun.misc.Launcher.AppClassLoader

2.1.2.1 扩展类加载器 extention class loader

  • 扩展类加载器的父是启动类加载器(bootstrap class loader)
  • 负责加载相对次要、但又通用的类,如JRE的lib/ext目录下jar包中的类(以及java.ext.dirs指定的类, 这个可以通过查看sun.misc.Launcher.ExtClassLoader.getExtDirs()方法确认)

2.1.2.2 应用类加载器 application class loader

  • 应用类加载器的父则是扩展类加载器
  • 负责加载应用程序路径下的类(应用程序指虚拟机参数-cp/-classpath、系统变量java.class.path或环境变量CLASSPATH所指定的路径。这个可以通过查看sun.misc.Launcher.AppClassLoader确认)
  • 默认情况下,应用程序中包含的类便是通过应用类加载器加载的。

2.1.3 双亲委派模型

  • 指的是一个类加载器接收到加载请求时,会先将请求转发给父类加载器,在父类加载器没有找到所请求的类的情况下,该类加载器才会去尝试加载。

  • 双亲委派模型可以避免类的重复加载,以及java的核心api被篡改的问题。

3 链接 Linking

  • 指将创建的类合并至Java虚拟机中,使之能够执行的过程。可分为验证(Verification)、准备(Preparation)以及解析(Resolution)三个阶段

3.1 验证 Verification

验证是为了确保被加载的类满足Java虚拟机的约束条件。

3.2 准备 Preparation

  • 准备是为被加载的类的静态字段分配内容。
  • 构造其他跟类层次相关的数据结构:如用来实现虚方法的动态绑定的方法表。

3.3 解析 Resolution

在开始解析之前,需要知道:
class文件被加载到Java虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java编译器会生成一个符号引用。在运行阶段,这个符号引用一般都能无歧义地定位到具体目标上。

  • 解析的目标是将符号引用解析成为实际应用: 如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析就触发这个类的加载。(但未必会出发这个类的链接和初始化)

此外,在Java虚拟机规范中并没有要求在链接过程中完成解析。仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

4. 初始化 Initialization

  • 为标记为常量值的字段赋值,以及执行<clinit>方法的过程
  • Java虚拟机会通过加锁来确保类的<clinit>方法仅被执行一次

常量值解释:
Java代码中,如果要初始化一个静态字段,可以在声明时直接赋值,或者在静态代码块中对其进行赋值
如果直接赋值的静态字段被final所修饰,并且它的类型是基本类型或字符串时,该字段便会被Java编译器标记为常量值(ConstantValue)

4.1 初始化的触发条件

在Java虚拟机规范中明确枚举了以下情况:

  • The execution of any one of the Java Virtual Machine instructions new, getstatic, putstatic, or invokestatic that references C (§new, §getstatic, §putstatic, §invokestatic). These instructions reference a class or interface directly or indirectly through either a field reference or a method reference.

  • Upon execution of a new instruction, the referenced class is initialized if it has not been initialized already.

  • Upon execution of a getstatic, putstatic, or invokestatic instruction, the class or interface that declared the resolved field or method is initialized if it has not been initialized already.

  • The first invocation of a java.lang.invoke.MethodHandle instance which was the result of method handle resolution (§5.4.3.5) for a method handle of kind 2 (REF_getStatic), 4 (REF_putStatic), 6 (REF_invokeStatic), or 8 (REF_newInvokeSpecial).

  • This implies that the class of a bootstrap method is initialized when the bootstrap method is invoked for an invokedynamic instruction (§invokedynamic), as part of the continuing resolution of the call site specifier.

  • Invocation of certain reflective methods in the class library (§2.12), for example, in class Class or in package java.lang.reflect.

  • If C is a class, the initialization of one of its subclasses.

  • If C is an interface that declares a non-abstract, non-static method, the initialization of a class that implements C directly or indirectly.

  • If C is a class, its designation as the initial class at Java Virtual Machine startup (§5.2).

Java虚拟机规范中有部分是依赖于Java虚拟机指令了,我对此了解并不多,以下摘抄于《极客时间-深入拆解Java虚拟机-郑雨迪》的分享,相较而言更通俗易懂些。

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

5. 绑定本机方法实现

指的是将Java编程语言以为的其他语言编写的功能和实现native方法的功能集成到Java虚拟机中以便可以执行的过程。

传统上来说,此过程可称为链接,但Java虚拟机规范中指出,使用绑定是为了避免于Java虚拟机对类和接口的链接产生混淆。

6. Java虚拟机退出

当调用Runtime.exit()Runtime.halt()System.exit,并且SecurityManager安全管理其允许exit或halt时候,Java虚拟机就会被关闭。

同时,JNI(Java Native Interface)规范描述了对于JNI调用相关的java虚拟机的终止信息。

参考文档

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
https://time.geekbang.org/column/article/11523