深入jvm 03. 类的加载过程是什么样的?

1、加载

类加载阶段中的"加载"工作,主要做三件事:1)通过一个类的全限定名来获取这个类的二进制字节流,这个字节流既可以是javac编译器生成的class文件,也可以是从压缩包中获取的字节流、从网络中获取的字节流、以及运行时计算生成的(比如动态代理技术);2)将这个字节流所代表静态存储结构转换为方法区的运行时数据结构;3)在内存中生成一个代表这个类的Class对象,作为方法区的这个类的各种数据的访问入口。

数组类没有对应的字节流,数组类本身不通过类加载器创建,是由jvm直接在内存中动态构造出来的。其他类(包括类和接口)则需要由类加载器来加载。自jdk1.2以来,java一直保持三层类加载器、双亲委派的类加载架构。

三层类加载器:(jdk9之前)
第一层是启动类加载器( bootstrap class loader )。它是由C++实现,所以没有对应的java对象,在java中只能用null来指代。它负责加载最基础、最重要的类(比如存放来lib目录下的jar包中的类)。
第二层是扩展类加载器( extension class loader ),它的父类加载器是启动类加载器。负责加载相对次要但通用的类(比如存放在lib/ext 目录下 jar 包中的类)。
第三层是应用类加载器( application class loader ),也称系统类加载器,它的父类加载器是扩展类加载器。负责加载应用程序路径下的类。
除上述三种类加载器,我们还可以加入自定义类加载器来实现特殊的加载方式,比如可以先对class文件加密,在加载时再利用自定义类加载器进行解密。

双亲委派模型
每当一个类加载器接到加载请求时,它会先将请求转发给它的父加载器,每一层都是如此,因此所有的加载请求最终都会传送到最顶层的启动类加载器中。如果父加载器没有找到所请求的类时,子加载器才会尝试自己去加载,如果子加载器完成不了,则子加载器的子加载器会去尝试加载。
在这里插入图片描述
为什么要进行双亲委派?
通过双亲委派模型组织类加载器的加载工作,可以使得java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。比如java.lang.String和自定义的一个类也叫String,自定义的这个String类将永远不会被加载,因为通过双亲委派,最终会由启动类加载器去加载java.lang.String这个类,这保证了String类在程序的各种类加载器环境中始终是同一个类,这对于java程序的稳定运作极为重要。

jdk9引入了模块系统,扩展类加载器改名为平台类加载器( platform class loader ),除了少数关键模块,如java.base中的类是由启动类加载器加载,其余模块都是由平台类加载器加载。

类的唯一性是由该类的全名和加载这个类的类加载器实例共同确定的。如果一串字节流由不同的类加载器加载,会得到不同的类(大型应用中可以借此来运行同一个类的不同版本)。

2、链接

链接可以分成三步:验证、准备和初始化

验证
主要工作是确保被加载的字节流能够满足jvm的约束条件

准备
主要工作是为被加载类的类变量(静态变量)分配内存并设置类变量初值。这个阶段对类变量的初值设置实际上只是**"零值"初始化**,即boolean类型设置为false,reference类型设置为null,其他类型设置为对应类型的0值。如果类变量是常量,那么在准备阶段就会被初始化为直接赋值的值

解析
主要工作是将符号引用解析为实际引用。在class文件被加载至jvm之前,它无法知道这个类所调用的其他类的方法、字段的具体地址,也不知道自己的方法、字段的地址,所以在引用这些成员的时候,javac编译器会生成一个符号引用。到了运行阶段,这些符号引用会被替换为实际引用,可以定位到目标的实际位置。如果符号引用指向一个未被加载的类、或者类的字段和方法,将触发这个类的加载(不一定会进行链接和初始化)。

3、初始化

初始化阶段是进行类变量的赋值和静态语句块的执行,即执行类构造器的()方法的过程。初始化是按照语句在源文件中出现的顺序进行的,静态语句块只能访问定义在静态语句块之前的变量,定义在静态语句块之后的变量可以在静态语句块中赋值,但不能访问。

子类的()方法执行前,jvm会保证其父类的()方法已经执行完毕。但是在接口中情况不同,执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量被使用到时,父接口才会初始化。接口的实现类在执行()前也不要先执行接口的()方法。

如果多个线程同时取初始化一个类,那么通过锁机制,只会有一个线程会去执行这个类的()方法,其他线程将会阻塞直到此类被初始化完毕,并且之后也不会再去执行()方法。因此,同一个类加载器下,一个类只会被初始化一次

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

元素为引用类型的数组被创建时会加载该数组元素所属的类,但不会链接和初始化这个类。因为jvm必须知道有这个类,才能创建这个类的数组,因此需要执行类的加载阶段,但由于还没有使用到这个类没有触发类的初始化条件),所以不会进行链接和初始化。

典型案例:单例模式(延迟初始化)

public class Singleton{
    
    
	private Singleton{
    
    }
    private static class LazyHolder{
    
    
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
    
    
        return LazyHolder.INSTANCE;
    }
}

这个案例对应了当调用静态方法或字段的指令时,才会初始化对应类。上述程序只有在调用getInstance()方法访问LazyHolder.INSTANCE时,才会初试化LazyHolder类,创建单例。在多线程环境下,类初始化是线程安全的,并且只会被执行一次。

猜你喜欢

转载自blog.csdn.net/Longstar_L/article/details/107464788
03.