JVM 知识 -- 类加载

本文最后更新于:1 个月前

类生命周期

  1. 加载

    1. 通过全类名获取二进制字节流 (可操控性最强的阶段,例如通过自定义类加载器去网络上获取字节流加载)

    2. 将字节流表示的静态存储结构变成方法区中运行时动态的存储结构

    3. 在内存中生成一个 Class 对象,作为方法区内访问数据的入口

  2. 连接

    注意:加载和连接阶段是交叉进行的

    1. 验证

      1. 验证文件格式

        这是验证字节流是否符合 Class 文件格式的规范

        例如字节码开头的 0xCAFEBABY、主次版本号是否在 JVM 处理范围内

      2. 验证元数据

        这是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求

        例如这个类是否有父类,这个类是否继承了不允许继承的类

      3. 验证字节码

        这是最复杂的阶段,通过数据流和控制流确定程序语义是合法的

        例如保证任意时刻操作数栈和指令代码序列都能配合工作

    2. 准备

      正式为类变量分配内存并设置初始值

      注意:

      1. 只是类变量而非实例变量

      2. 初始值指的是零值,具体指定的值在初始化阶段才会赋值

      3. 上面的条件二在常量(被 final 修饰的变量)时不成立,在准备阶段就会赋予目标值。

    3. 解析

      JVM 将常量池中的符号引用替换为直接引用,也就是得到类或者字段、方法在内存中的指针或者偏移量

      主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符等

      直接引用是指向目标的指针、相对偏移量、间接定位到目标的句柄

      例如 JVM 要去调用某一个方法,需要知道这个方法在方法表中的偏移量,而每个类都有一个方法表

  3. 初始化

    类加载的最后一个阶段

    只有当下面几种情况才会初始化:

    1. 遇到 newgetstaticputstaticinvokestatic 这几条字节码指令 (new 一个对象、读取静态字段、给静态字段赋值、调用类的静态方法)

    2. 反射调用

      Class.forName(),newInstance()

    3. 初始化子类时先初始化父类

    4. 包含 main() 方法的那个主类

  4. 使用

  5. 卸载

    一个类被卸载的必要条件:

    1. 堆上没有这个类的实例对象

    2. 类没有被引用

    3. 该类的类加载器已经被回收

      JDK 自带的类加载器加载 JDK 自带的类,不会被回收,只有自定义的类加载器的实例才可以被回收

类加载器

几种类加载器

  1. BootstrapClassLoader(启动类加载器)

  2. ExtensionClassLoader(扩展类加载器)

  3. ApplicationClassLoader (应用程序类加载器、系统类加载器)

  4. 自定义类加载器

    上面除了 BootstrapClassLoader 类加载器,其他的 ClassLoader 都必须继承自 java.lang.ClassLoader 类,而 Bootstrap ClassLoader 不继承自 ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader 也随着启动

    (继承 ClassLoader 默认父类加载器是 ApplicationClassLoader)

    不继承 AppClassLoader 的原因是:它和 ExtensionClassLoader 都是 Launcher 的静态类,都是包访问路径权限。

双亲委派模型

  1. 一个类被加载时,首先判断是否被加载过,如果没有则会先去委派父类加载器处理,因此所有的请求最终都会传到 BootstrapClassLoader 加载器,当父类加载器无法加载的时候才会向下交给子类加载器去加载,如果子类也不能加载就再向下委托,如果最后的子类加载器都无法加载则会抛出 ClassNotFoundException

  2. 注意:

    1. 双亲委派模型实际上不是有两个上级加载器的意思,父类加载器只有一个

    2. 类加载器的父子关系并不是继承实现的,而是通过优先级实现的

  3. 为什么有双亲委派模型

    1. 安全

      当我们写一个自定义的 String 类型时,保证 JVM 加载的还是 JDK 自带的类,因为 BootstrapClassLoader 类加载器就能把这个类加载出来了,ApplicationClassLoader 不会去加载我们自定义的 String 类

      即使是我们自己用自定义类加载器去加载,也不会成功,因为 String 类在 JVM 启动时已经被 BootstrapClassLoader 加载了,不会二次重复加载

类加载器加载流程

类加载器加载的时候,就是调用 loadClass() 方法:

  1. 首先检查这个类是否被加载过

  2. 如果有,直接返回,否则开始加载

  3. 找到父类加载器,如果不为空,说明父类加载器不是 BootstrapClassLoader,那么直接调用父类加载器的 loadClass() 方法;否则父类加载器就是 BootstrapClassLoader,

注意

如果类加载器不同,那么即使是同一份字节码交给两个不同的类加载器去加载,得到的类也是不同的

为什么需要自定义类加载器(打破双亲委派)

  1. 字节码加密解密

  2. 动态获取类(热部署)

  3. 从特定源获取类(例如网络,而非本地文件系统)

例子:

  1. 例如 Tomcat 定义多个类加载器:

    1. 保证同一个服务器的两个 web 应用程序 Java 类库隔离

    2. 保证同一个服务器的两个 web 应用程序 Java 类库可以共享

    3. 保证服务器自身安全不被 web 应用程序影响

  2. 再比如数据库驱动类

    Class.forName("com.mysql.dj.Driver");


    先多说一句这段代码的原理是:mysql 驱动的 Driver 类里面有一个静态代码块,当 Driver 类被加载的时候这段静态代码块被执行,将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。

    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        public Driver() throws SQLException {
        }
    
        static {
            try {
                DriverManager.registerDriver(new Driver());//首先new一个Driver对象,并将它注册到DriverManage中
            } catch (SQLException var1) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    }

    而我们之后一般又会这样链接数据库

    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");


    上面的Class.forName() 方法是使用调用者 Class 对象的 ClassLoader 来加载目标类,但是这样其实没有打破双亲委派,具体的分析看最后的 SPI 扩展相关部分吧

类加载器主要方法

  1. loadClass

    加载类的方法入口,这个方法默认就遵循双亲委派模型加载类,如果想要破坏双亲委派模型,就要重写这里的方法

  2. findClass

    如果上面的 loadClass 方法都没有办法加载出来,就会调用这个类加载器自己的 findClass 方法加载。

    对于最下层的类加载器来说,如果此处还是返回为空,说明这个类无法找到

  3. defineClass

    如果类加载器加载了类,获取到字节码,就用这个方法将字节码转换为 Class 对象

class ClassLoader {

  // 加载入口,定义了双亲委派规则
  Class loadClass(String name) {
    // 是否已经加载了
    Class t = this.findFromLoaded(name);
    if(t == null) {
      // 交给双亲
      t = this.parent.loadClass(name)
    }
    if(t == null) {
      // 双亲都不行,只能靠自己了
      t = this.findClass(name);
    }
    return t;
  }
  
  // 交给子类自己去实现
  Class findClass(String name) {
    throw ClassNotFoundException();
  }
  
  // 组装Class对象
  Class defineClass(byte[] code, String name) {
    return buildClassFromCode(code, name);
  }
}

class CustomClassLoader extends ClassLoader {

  Class findClass(String name) {
    // 寻找字节码
    byte[] code = findCodeFromSomewhere(name);
    // 组装Class对象
    return this.defineClass(code, name);
  }
}

扩展: SPI

在高版本的JDK,已经不需要手动调用class.forName方法了,在DriverManager的源码中可以看到一个静态块

/**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 重点!!!
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

loadInitialDrivers 方法中,最重要的是 ServiceLoader.load(Driver.class),这行代码可以把类路径下所有 jar 包中 META-INF/services/java.sql.Driver 文件中定义的类加载上来,此类必须继承自java.sql.Driver

其次,driversIterator.next();还会循环调用上面获取到所有的 Driver 的子类,调用 next 方法,最后调用下面的 nextService 方法:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //重点!使用当前示例的成员变量loader加载,就是上面设置的ThreadContextClassLoader
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
       fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
            x);
    }
    throw new Error();          // This cannot happen
}

上面的 Class.forName() 方法完成了在 BootstrapClassLoader 加载的类(Driver.class)中通过ThreadContextClassLoader 加载了应用程序的实现类。

这段代码的重点就是 Class.forName()

也就是说,通过SPI的方式把程序员手动做的动作(Class.forName())变成框架自动执行。

相关博客

所以,SPI (service provider interface)其实是一种标准,他定义了接口,而不同服务的厂商去实现这个接口,当用户需要使用的时候,就把相关的实现类放到指定的位置,应用程序会自动找到他们并加载

相比 API,SPI 更接近服务调用方

在 jdk6 里面引进的一个新的特性 ServiceLoader,从官方的文档来说,它主要是用来装载一系列的 service provider。而且 ServiceLoader 可以通过 service provider 的配置文件来装载指定的 service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在 jar 包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

所以,SPI 的思想是:接口的实现还是由服务提供者来实现,但是服务提供者只用在提交的 jar 包里的 META-INF/services 下根据平台定义的接口新建文件,并添加进相应的实现类内容就好,而服务调用者根据标准接口通过本地服务发现加载具体服务提供商。

这样做的好处是:实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

常用场景有:

  1. 数据库驱动实现类加载

    JDBC 加载不同的数据库驱动

  2. 日志门面接口加载

    Slf4j 加载不同日志实现

  3. Spring