欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

深入浅出--类加载器和类加载机制

最编程 2024-03-09 10:01:01
...

类加载器(ClassLoader)是负责读取 Java 字节码,并转换成 java.lang.Class 类的一个实例的代码模块。
类加载器除了用于加载类外,还可用于确定类在Java虚拟机中的唯一性。在整个JVM里,纵然全限定名相同,若类加载器不同,则仍然不算作是同一个类,无法通过 instanceOf 、equals 等方式的校验。

1. 类加载器

类加载器层级关系如下图所示, Bootstrap、Extension、Application 三者并非是继承关系,而是子类加载器指派父类加载器为自己的 parent属性。由于启动类加载器是内嵌于 JVM 且无法被引用,因此 Extension Classloader 设置null为parent,即等同于指派启动类加载器为自己的父加载器。

image.png

类加载器

1.1 Bootstrap ClassLoader
由C++语言实现的,并不是继承自java.lang.ClassLoader,没有父类加载器。
加载Java核心类库,如:$JAVA_HOME/jre/lib/rt.jar、resources.jar、sun.boot.class.path路径下的包,用于提供JVM运行所需的包。
它加载扩展类加载器和应用程序类加载器,并成为他们的父类加载器。

1.2 Extension ClassLoader
Java语言编写,继承自java.lang.ClassLoader,父类加载器为启动类加载器。
负责加载java平台中扩展功能的一些jar包。从系统属性:java.ext.dirs目录中加载类库,或者从JDK安装目。录:jre/lib/ext目录下加载类库。我们可以将我们自己的包放在以上目录下,就会自动加载进来了。

1.3 Application ClassLoader
Java语言编写,继承自java.lang.ClassLoader,父类加载器为启动类加载器
它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库
是程序中默认的类加载器,我们Java程序中的类,都是由它加载完成的。
我们可以通过ClassLoader#getSystemClassLoader()获取并操作这个加载器。

1.4 User ClassLoader
Java语言编写,根据自身需要实现ClassLoader自定义加载class,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。通过灵活定义classloader的加载机制,我们可以完成很多事情,例如解决类冲突问题,实现热加载以及热部署,甚至可以实现jar包的加密保护。实现自定义ClassLoader的示例参考章节3自定义类加载器

**

  // App ClassLoader
  System.out.println(this.getClass().getClassLoader());
  // Ext ClassLoader
  System.out.println(this.getClass().getClassLoader().getParent());
  // Bootstrap ClassLoader
  System.out.println(this.getClass().getClassLoader().getParent().getParent());
  // Bootstrap ClassLoader
  System.out.println(new String().getClass().getClassLoader());

输出结果如下,Bootstrap ClassLoader属于JVM的范畴,所以是null

**

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3ac42916
null
null

3. 加载类的方式

1. 隐式加载
通过new隐式加载,创建对象时,如果类未加载也会尝试加载,例如 Student s = new Student();会尝试加载Student
2. 显示加载:

  1. 通过Class.forName(类全限定名)加载

**

    public static Class<?> forName(String className) throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

其中forName0() 方法调用中的参数 true 表示要初始化该类。包括:

  • 执行静态代码块
  • 初始化静态域

例如com.mysql.jdbc.Driver就通过静态代码块想DriverManager中注册自己。当然forName(String name, boolean initialize,ClassLoader loader)也支持指定初始化(initialize)和类加载器(loader)

  1. 通过ClassLoaderloadClass()方法加载类

4. 类加载机制

4.1 全盘负责
当一个类加载器负责加载某个类时,该类所依赖的和引用的其他类也将由该类加载器负责加载,除非显示使用另外一个类加载器来加载。
4.2 双亲委派
双亲委派(Parent Delegation),是一个非常糟糕的翻译,但是因为使用较广所以一直沿用至今, 双亲委派也叫作“父类委托”,是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并加载目标类。
双亲委派的机制如ClassLoader中loadClass方法所示:

  1. findLoadedClass,内部调用native方法,在虚拟机内存中查找是否已经加载过此类
  2. 如果没有加载,则委派父类加载,对于Extension ClassLoader,parent是null,所以通过findBootstrapClassOrNull委派Bootstrap ClassLoader加载。
  3. 如果父类没有加载,则通过findClass(name)自己尝试加载。

**

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

5. 自定义类加载器

“双亲委派”机制只是Java推荐的,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,只需要重写findClass(name)方法;如果想破坏双亲委派模型,则重写loadClass(name)方法。
如下MyClassLoader,通过重写findClass,从MyClassLoader.setRoot 指定的目录加载编译后的.class 文件。注意文件路径不能是classpath路径, 防止要加载的类被Application ClassLoader加载。

**

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        // fileName处理逻辑需要根据实际情况修改,保证能够找到文件
        String[] name = className.split("\.");
        String fileName = root + File.separatorChar + name[name.length - 1] + ".class";
        try {
            return Files.readAllBytes(Paths.get(fileName));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("指定目录");
        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("类的全限定名");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
            System.out.println(object.getClass().getClassLoader().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6. SPI机制是否打破双亲委派

关于SPI(Service Provider Interface)是否打破双亲委派,众说纷纭,这里先抛出结论:没有打破双亲委派
双亲委派机制是指,子类加载器加载类之前,先去父类加载器中查找,一直查到最基础的启动类加载器,如果都没有加载,则尝试自行加载。根据双亲委派原理,父类加载器加载的类对子类可见,反之则不成立。
这里以JDBC(Java Database Connectivity,Java数据库连接池)为例进行说明。使用JDBC的示例代码如下:

**

String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
Connection conn = DriverManager.getConnection(url, "root", "1234");

1. DriverManager加载
根据包名可知java.sql.DriverManager是由启动类加载器加载,在加载时,通过静态代码块调用loadInitialDrivers()方法, loadInitialDrivers()通过SPI的方式加载java.sql.Driver的实现,这里是com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver,加载过程如下,省略部分非核心代码:

**

    private static void loadInitialDrivers() {
        String drivers;

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

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

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
    }

2. java.mysql.jdbc.Driver 加载
SPI的机制这里不展开讲,核心原理是通过mysql-connector-java.jar中的META-INF/services/java.sql.Driver文件,找到java.sql.Driver的具体实现,然后加载实现。
loadInitialDrivers()中的load() 实现如下所示,注意通过Thread.currentThread().getContextClassLoader()获得应用类加载器,赋值给loader成员变量。

**

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

SPI中,最终通过Thread.currentThread().getContextClassLoader()获得的应用类加载器加载com.mysql.jdbc.Driver,代码如下:

**

        try {
            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;
        }

cn这里就是com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver。调用c.newInstances时, 会执行com.mysql.jdbc.Driver中的静态代码块,即向DriverManager注册自己。

以上穿插的源码比较多,又夹杂着SPI的源码,所以比较混乱,这里做一下总结:

  1. java.sql.DriverManager 是由启动类加载器加载,在加载时,通过SPI加载java.sql.Driver的实现,即com.mysql.jdbc.Driver

  2. com.mysql.jdbc.Driver是由应用类加载器负责加载的

  3. 父类加载器(Bootstrap ClassLoader)通过线程上下文类加载器(ContextClassLoader)去请求子类加载器(Application ClassLoader)完成类加载的行为,看似打破了双亲委派模型来逆向使用类加载器,晚上所有的打破双亲委派也是指这一过程。但是子类加载器的加载也是走双亲委派流程,先委托给父类加载器,加载不到再尝试自行加载,因此完全没有破坏双亲委派。

image.png

推荐阅读