讲类的加载机制

0,写在前面

最近在学习 Tomcat 的内部知识,了解到 Tomcat 也打破了双亲委派模型,想到之前 springBoot 的启动流程也是通过 SPI 机制破坏了双亲委派模型,因此觉得有必要总结一下类加载机制的原理。

1,类的加载流程

讲类的加载机制之前,有必要了解一下类的生命周期,包括:加载,验证,准备,解析,初始化,使用,卸载这 7 个阶段。


java自定义类加载器 顺序_java

加载阶段:方式包括:通过一个类的全限定名,可以从本地 jia 包,resource、网络 URL 等途径加载。加载后,会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的访问入口。

链接阶段:

  • 验证(Verify)
    确保 Class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性;主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
  • 准备(Prepare)
    主要工作是为类变量分配内存(到方法区)并且设置该类变量的默认初始值,即零值。这里的分配内存不包含用 final 修饰的类变量,因为其在编译的时候就会被分配,准备阶段会将其显式初始化。这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到 Java 堆中。
  • 解析(Resolve)
    主要工作是将常量池内的符号引用转换为直接引用的过程。符号引用就是一组符号来描述所引用的目标。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等。

初始化阶段:对类的静态变量,静态代码块执行初始化操作。初始化时机:使用 new,调用类的静态方法,读取类的静态字段,反射

卸载阶段:虚拟机退出,程序结束

2,类的加载机制

先来概括一下,什么是类的加载器,他是一个通过全限类定名获取一个类的二进制数据流,并将该流转换成对应类的一套代码或程序。

类的加载器一般我们分为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。这里的自定义类加载器其实包括扩展类加载器(Extension ClassLoader)和系统类加载器(AppClassLoader)。

Bootstrap 是由虚拟机负责的加载底层由 c、c++代码实现的核心库。如<JAVA_HOME>\lib 目录;

Extension,加载对象是 <JAVA_HOME>/jre/lib/ext 目录里面的类;

Application,也叫 System ClassLoader,加载用户类路径 classpath 的类库。

java自定义类加载器 顺序_spi_02

双亲委派模型 && 缓存机制

类的加载机制是一层层向上委托加载的(先找父加载器加载),即按照 AppClassLoader->Extension ClassLoader->Bootstrap ClassLoader 的委托形式,只有当根类 Bootstrap ClassLoader 无法加载时,才会自己尝试去加载,否则抛出 ClassNotFindException。另外,一个类中引用的类也会遵循该类的加载器机制。


java自定义类加载器 顺序_双亲委派机制_03

这种机制的优势是显而易见的:

  • 避免类的重复加载,由上级加载器优先加载,加载过后子类无需再次加载。
  • 沙箱安全机制,核心 API 必须走系统的实现类,防止恶意篡改。

与双亲委派机制相对应的就是全盘委托机制,即一个类自始至终都为一个 ClassLoader 负责,除非显示使用其他的 ClassLoader。

类加载的方式:(获取 Class 类对象的方式)

  • 通过 Class.forName() 方法动态加载,默认会初始化类
  • 通过 ClassLoader.loadClass() 方法动态加载。

3,自定义类加载器

API:

  • loadClass():就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中
  • findClass():根据名称或位置加载.class 字节码
  • definclass():把字节码转化为 Class

1,不破坏双亲模型,只需继承 ClassLoader,重写 findClass 方法即可。

双亲委派的逻辑主要是在 loadclass 方法,然后检查这个类是否会被加载,没有加载则找父类加载器,递归后父类加载器没有加载,再自己调用 findClass 方法加载这个类。很明显,我们只要重写这个方法就好。

/**
 * 被加载类,初始化时输出类的加载其
 */
public class Test {
    public Test(){
        System.out.println(this.getClass().getClassLoader().toString());
    }
}
package me.lsk.test;

import java.io.*;

/**
 * 自定义不打破双亲机制的类加载器,只需继承 ClassLoader 然后重写 findClass 方法,改变加载顺序即可。
 * 先加载 Test.class
 */
public class MyClassLoader extends ClassLoader {

    private String name;
    public MyClassLoader(ClassLoader parent, String name){
        super(parent);
        this.name = name;
    }
    @Override
    public String toString(){
        return this.name;
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        MyClassLoader loader = new MyClassLoader(MyClassLoader.class.getClassLoader(), "MyClassLoader");
        Class aClass = loader.loadClass("me.lsk.test.Test");
        // newInstance 根据无参构造生成对象
        Object object = aClass.newInstance();
        // 会执行构造方法
    }

    /**
     * 获得类的 Class 对象
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    public Class<?> findClass(String name){
        InputStream in = null;
        byte[] data = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            in = new FileInputStream(new File("D:/Test.class"));
            int c = 0;
            while(-1 != (c=in.read())){
                baos.write(c);
            }
            data = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                in.close();
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 输出指定名称的 class
        return this.defineClass(name,data,0,data.length);
    }
}
// 输出
sun.misc.Launcher$AppClassLoader@18b4aac2

2,破坏双亲模型,继承 ClassLoader 后重写 loadClass 和 findClass 方法。

loadClass 决定了先加载父类的逻辑,重写他就可以打破双亲委派逻辑。

// 在 MyClassLoader 里补充以下逻辑:
 @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        ClassLoader loader = getSystemClassLoader();
        Class<?> aClass = null;
        aClass= loader.loadClass(name);
        if(aClass!=null) return aClass;
        // 加载不到再由 findclass 走双亲机制
        aClass = findClass(name);
        return aClass;
    }

4,如何打破双亲委派模型

SPI 机制

什么是 SPI(Service Provider Interface)?它是 JDK 内置的一种服务提供发现机制,通过 JDK 提供接口,第三方实现和扩展 API。动态替换发现的能力,实现了接口和实现的解耦。

如何使用?我们在 META-INF/services 下写入要加载的类的全限定文件名,然后就可以使用 serviceloder 工具类加载,即可获得一个迭代器集合。原理?通过 ContextClassLoader 线程上下文类加载器,通过改变加载顺序,破坏双亲委派机制,解决了第三方类库的加载问题。

JNDI 机制

什么是 JNDI(Java Naming and Directory Interface)?JAVA 命名和目录接口,使得我们可以通过路径名方便的访问资源。通过 JNDI,把一个 Java 对象和一个特定的名称关联在一起,方便容器后续使用,实现了解耦。

5,框架里的应用

5.1,Tomcat

Tomcat 为什么要破坏双亲机制?

每个 Tomcat 需要支持部署多个应用,不同应用可能依赖同一类库,也可能依赖不同类库,也可能依赖不通类库的不同版本。这也就意味着 web 应用资源间既需要共享也需要隔离。如果依然遵从 JDK 的双亲委派机制,那么整个系统只能缓存一份类库,这显然是不能支持不同应用依赖不同类库的需求。所以 Tomcat 需要打破该机制。

Tomcat 是如何做的?

Tomcat 破坏双亲委派,通过提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。每个应用先使用自己的类加载器 WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交给公共资源加载器 CommonClassLoader 加载,他再按照向上加载的原则,交给系统类加载器、扩展类加载器、启动类加载器加载。

这和双亲委派刚好相反。这样做也使得 Tomcat 具备热插拔功能。


java自定义类加载器 顺序_双亲委派机制_04

5.2,SpringBoot

在 springboot 的自动装配过程中,最终会加载 META-INF/spring.factories 文件,而加载的过程是由 SpringFactoriesLoader 加载的。从 CLASSPATH 下的每个 Jar 包中搜寻所有 META-INF/spring.factories 配置文件,然后将解析 properties 文件,找到指定名称的配置后返回。

5.3,JDBC

JDK 面向不同的数据服务商提供了统一的驱动接口,然后我们的数据库厂商就可以自定义驱动实现接口,从而创建连接,如 JDBC,ODBC 等方式。这里的 DriverManager 会由启动类加载器加载,但是里面调用的 Drier 类的,是第三方实现的,只能由系统类加载器加载 classpath 获得。然而,启动类加载器是不能向下委托加载的,所以这里就需要破坏这种双亲委派机制了。

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

JDBC 通过 SPI 机制,利用上下文加载器,获得当前的加载器 AppClassLoader 实现驱动加载,这里的驱动在META-INF/services/java.sql.Driver

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
....
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

6,总结

这一节,我们从 JVM 的类加载机制聊起,包括类的生命周期、自定义类的加载器,以及双亲委派机制。

同时讨论了双亲委派这样做的好处?单向加载带来的弊端。一些框架中破坏双亲委派的典型应用等。