Java 虚拟机中的类加载,按先后顺序需要经过加载、链接以及初始化三大步骤。
其中,链接过程中同样需要验证;而内存中的类没有经过初始化,同样不能使用。那么,是否所有的 Java 类都需要经过这几步呢
在Java语言中,类型可以分为两大类:基本类型与引用类型。其中基本类型都是有Jav虚拟机预先定义好的。而引用类型又可以细分成四种:类,接口,数组类和泛型参数。由于泛型参数在编译过程中会被擦除,所以,对于Java虚拟机来说,实际上只有前三种类型。在类、接口和数组中,数组类是由Java虚拟机直接生成的,其他两种则有对应的字节流。字节流(也就是java编译器编译成的class文件)都会被加载到java虚拟机中,成为类或者接口(以下统称类)。
但是,无论是虚拟机直接生成的数组类,还是加载的类,java虚拟机都需要对其进行链接和初始化。
加载
加载,就是指查找字节流,并且创建类的过程。对于数据类来说,他没有对应字节流,由java虚拟机直接生成。对于其他类来说,就需要java虚拟机借助类加载器来完成字节流查找的过程。
关于类加载器主要分为三种,启动类加载器,扩展类加载器以及应用类加载器。在java虚拟机中,每当有一个类加载器接收到加载请求是,它会先将请求转发给父类加载器,在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。这样的加载方式我们称之为双亲委派模型。
在Java9之前,启动类加载器负责加载最为基础、最为重要的类(JRE的lib目录下jar包中的类)。扩展类加载器的父类加载器是启动类加载器,负责加载次要的但又通用的类(JRE的lib/ext目录下jar包中的类)。最后的应用类加载器的父类加载器是扩展类加载器,负责加载应用程序路径下的类。java9之后,扩展类加载器更名为平台类加载器,除了少数几个关键模块由启动类加载器加载外,其他模块均由平台类加载器所加载。
链接
链接,是指将创建成的类合并至Java虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
验证:验证的目的在于被加载的类能够满足Java虚拟机的规范。通常来说,通过Java编译器生成的类文件一定满足Java虚拟机的规范。(如果存在编译成字节码之后,反汇编修改了字节码的情况。可能就不满足Java虚拟机规范)
准备:准备阶段则是为被加载的类的静态字段分配内存。Java代码中对静态字段的具体初始化,则会在接下来的初始化阶段中完成。当然,准备阶段除了会为静态字段分配内存之外,有些虚拟机还会例如实现虚方法的动态绑定的方法表等等。
解析:在class文件被加载至Java虚拟机之前,这个类无知道其他类以及其方法、字段所对应的具体地址,甚至也不知道自己方法、字段的地址。所以,Java编译器在遇到需要引用成员时,会为其生成一个符号引用。在运行阶段,就可以通过该符号引用无歧义地定位到具体的目标之上。而解析阶段的目的就是讲这些符号引用解析成为实际地址引用。当然,如果符号引用指向了一个未被加载的类,或者未被加载的字段、方法,那么解析将会触发这个类的加载(但不一定会触发这个类的链接以及初始化的过程)
PS:Java虚拟机规范中并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要对这些字节码符号引用进行解析。
初始化
类加载的最后一步便是初始化了。在Java代码中,如果静态字段被final修饰,并且它的类型是基本类型或字符串时,那么该字段便会被Java编译器标记成常量值,其初始化过程由Java虚拟机直接完成。除此之外的赋值操作,以及所有静态代码块中的代码,则会被Java编译器优化到同一方法中,并把它命名为 <clinit >。初始化完成之后,类便成为可执行的状态。
关于类的初始化触发条件:
1.当虚拟机启动时,初始化用户指定的主类;
2.当遇到用以新建目标类实例的new指令时,初始化new指令的目标类;
3.当遇到调用静态方法的指令时,初始化该静态方法所在的类;
4.当遇到访问静态字段的指令时,初始化该静态字段所在的类;
5.子类的初始化会触发父类的初始化;
6.使用反射API对某个类进行反射调用时,初始化这个类;
实例操作:
新建Singleton.java
public class Singleton {
private Singleton() {}
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
static {
System.out.println("LazyHolder.<clinit>");
}
}
public static Object getInstance(boolean flag) {
if (flag) return new LazyHolder[2];
return LazyHolder.INSTANCE;
}
public static void main(String[] args) {
getInstance(true);
System.out.println("----");
getInstance(false);
}
}
运行命令
javac Singleton.java
java -verbose:class Singleton
PS:-verbose:class可以打印类加载的先后顺序
从运行结果可以看出调用getInstance(true)时,也就是新建LazyHolder数组时,只加载的类,并没有初始化。初始化是在调用getInstance(false)时完成。同时也可以通过openjdk工具包中下载asmtools.jar工具来修改字节码,得出在修改了编译后的字节码后,调用getInstance(true)时也不会调用链接的过程(因为链接的第一步就是验证字节码是否符合JVM规范).
总结:新建引用类型类的数组时,只会加载,不会链接和初始化。链接和初始化在真正实例化的时候会触发。