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. 遇到 new,getstatic,putstatic,invokestatic 这几条字节码指令 (new 一个对象、读取静态字段、给静态字段赋值、调用类的静态方法)

    2. 反射调用

      Class.forName(),newInstance() 等

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

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

    5. …

  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