JVM类加载机制

87 阅读6分钟

前置知识

在JVM的类加载中,从class文件到内存中的类,需要经过加载(load)-链接(link)-初始化(init)三大步骤。链接的过程需要进行验证,而内存中的类没有经过初始化也是不能够进行使用的。

Java语言的类型可以分为基本类型和引用类型,基本类型是Java中事先定义好的,由JVM预先定义好的,引用类型则需要使用到字节流才能够加载到JVM里面。引用类型在Java里面可以大致分为4种:类、接口、数组类(这里说的类是类在内存的一种数据结构,由JVM直接生成的,无需使用类加载器将其class文件加载到内存中,因为它没有对应的类加载和对应的字节流class文件)、泛型参数。泛型参数在编译阶段会被擦除,因此JVM实际上只有前三种,数组类由JVM直接生成,其他两种则有对应的字节流class文件。

说到字节流,最常见的形式是Java编译器生成的class文件,同时我们也可以从网络或者程序内部获取字节流,这些不同的字节流会被加载到JVM中成为类或接口。编译流程为: compiler-jvm-cpu,编译器会将程序员编写的源代码.java转换为字节码,JVM会将字节码变化为X64CPU(以X64为例,实际会根据不同cpu类型而灵活转换)使用的本地代码,然后由CPU负责实际的处理。不论是直接生成数组类还是加载的类,JVM都要进行链接和初始化。

加载

加载是指查找字节流并根据字节流创建类的过程,这个class类对象就是这个类各种数据的访问入口,说人话即:把代码数据加载到内存中。对于数组类来说它并没有对应的的字节流,而是由JVM直接生成的,对于其他类而言,JVM需要借助ClassLoader来完成查找字节流的过程。

Java中存在非常多的class loader类加载器,但是都有公共的祖师爷 启动类加载器(bootstrap class loader,由C++编写,没有对应的Java 对象,因此在Java只能用Null来表示)。除了启动类加载器外,其他类加载器都是java.lang.ClassLoader的子类,因此都存在对应的Java对象。这些类加载器需要实现由另外一个类加载器(比如 bootstrap class loader)加载到到JVM中,才能够执行类加载。

每当一个类加载器接收到加载请求时,会先将请求转发给父类加载器,在父类加载器没有找到的情况下,该类加载器才会尝试去加载,这种方式成为双亲委派模型,目的是避免重复加载Java类型。在Java9以前,启动类加载器负责加载最基础、最重要的类,除了启动类加载器外还有两个核心的类加载器分别为 扩展类加载器(extension class loader)和应用类加载器(application class loader),三者的关系 启动类加载器—>扩展类加载器—>应用类加载器。

扩展类加载器负责加载相对次要,但是通用的类。应用类加载器负责加载应用程序路径下的类(这里的路径是指代JVM -cp/-classpath、系统变量java.class.path或环境变量classpath),默认情况下,应用程序中的包含的类就是由应用类加载器加载的。Java9以后引入了模块系统,并将扩展类加载器重命名为平台类加载器,Java SE的除了少数及格模块,其他都由平台类加载器进行加载。

链接

链接就是将创建成的类合并到JVM中,使之能够运行的过程,可分为验证、准备以及解析三个阶段。

验证的目的是确保被加载的类能够满足JVM的约束条件,一般来说,Java编译器生成的类文件必然是满足JVM约束条件的。

准备的目的是给被加载类的静态字段分配内存。在class文件被加载JVM前,当前类是无法知道其他类及其方法、字段对应的具体地址,因此当需要引用这些对象的时候,Java编译器会生成一个符号引用,无歧义的指向具体的目标。比如对于一个方法的调用,Java编译器会生成一个包含方法所在类名称、目标方法的名称、方法参数类型、返回值类型的符号引用,来指代要调用的方法(符号引用:这是XXX的住的地方,不管是否存在我们都可以用这种说法来表示它住的地方,实际引用就是实际的通信地址了,比如购买快递的地址)。

初始化

在Java中,我们初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中进行赋值。如果直接赋值的静态字段被final修饰并且类型为基本类型或String类型时,那么这个字段将会被标记为常量,初始化工作直接由Java编译器完成。初始之外的初始化操作都会被Java编译器放置在名为 clinit 的方法中。

类加载的最后一步就是初始化,便是为被标记为常量的字段进行赋值和指向clinit方法,JVM会通过加锁的方式执行clinit方法并且确保只会被执行一次。当类完成初始化后,才是可执行的状态。

类初始化时机有以下多种情况:

  • 主类
  • 主动 new、静态调用、静态引用
  • 子类初始化触发父类初始化
  • default定义的接口
  • 反射调用、MethodHandle实例

总结

今天学习了Jvm将字节流转换为类的过程,这个过程可以分为类加载、链接、初始化三大过程。

加载是查找字节流并据此创建类的过程。加载需要借助类加载器,JVM中使用了双亲委任模型(这里不要理解为两个父类,而是指子到父、父到子的两个过程),即收到加载请求时,先将请求发送给父类加载器。

链接是指将创建的类合并在JVM中,并使之能够执行的过程。

初始化是将标记为常量的字段直接赋值,然后执行 clinit方法的过程。类的初始化只会被加载一次,这个特征被用于单例模式的延迟初始化。

public class Singleton {
    private Singleton() {
    }

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

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