浅谈Java自定义类加载器及JVM自带的类加载器之间的交互关系

浅谈Java自定义类加载器及JVM自带的类加载器之间的交互关系

Java类加载器负责将类的字节码从磁盘上读取到JVM内存中,并为类创建JVM运行时数据结构。JVM自带三种类加载器:启动类加载器、扩展类加载器和应用程序类加载器。Java自定义类加载器可以根据特定的需求实现不同的类加载行为和策略。

Java类加载器间的层次关系

Java类加载器中有一个明确的层次关系,如下图所示:

graph LR
    BootClassLoader --> ExtClassLoader
    ExtClassLoader --> AppClassLoader
    AppClassLoader --> UserDefinedClassLoader

先介绍JVM自带的类加载器:

启动类加载器

启动类加载器是Java虚拟机内置的类加载器,它是用来加载Java的核心库,位于JRE的lib目录下,且很少发生变化。它是所有类加载器层次结构的根,由C++实现,并不是Java类。启动类加载器无法被Java程序直接引用,因此Java程序中也无法获取到它的类对象。启动类加载器使用Bootstrap Classpath,即系统类路径,加载类。

扩展类加载器

扩展类加载器是用来加载Java平台扩展中的一些类。它位于JRE的lib目录下的ext子目录中,与启动类加载器并列。扩展类加载器使用Extension Classpath,即扩展类加载路径,加载类。

应用程序类加载器

应用程序类加载器是用来加载Java应用程序的类,所以也叫作系统类加载器。它是classpath环境变量所指定的目录中加载类。应用程序类加载器是一种常见的Java类加载器,开发者可以直接使用它。

再介绍Java自定义类加载器:

用户自定义类加载器

用户自定义类加载器是开发者提供的一种自定义实现类加载机制的方式,用来满足一些特殊的需求。用户自定义类加载器需要继承ClassLoader类,并重写它的两个重要方法 findClass(String name)和 loadClass(String name, boolean resolve)。其中findClass方法用于查找和加载类,如果进行自己的方式查找和加载失败,则直接调用父类的loadClass方法进行加载。loadClass方法用于加载类,负责在类还未被加载时,首先调用父类的loadClass方法进行加载。

Java类加载器间的协作

Java类加载器间的交互是通过父子关系实现的,每个类加载器在加载类时,先委托其父类加载器寻找该类,其它时候父类加载器不会干涉子类加载器的加载过程。当一个类加载请求到达某个类加载器时,该类加载器会先判断该类是否已经被加载过,如果没有被加载过,则该类加载器会委派其父类加载器来完成该任务。如果父类加载器不能完成,则当前类加载器才会尝试自己加载。因此,类加载器的协作关系是一种树状结构,组成了Java类加载器的层次结构。

下面给出两个实例说明类加载器间的交互关系:

示例1:父子类加载器优先级问题

假设我们自定义一个类加载器MyClassLoader,它继承自ClassLoader类,用于加载MyClass类。当前系统中使用的是应用程序类加载器,即MyClassLoader的父类加载器。那么,在程序执行过程中调用MyClassLoader类的loadClass(String name)方法时,该方法会调用到父类的loadClass方法,最终会由应用程序类加载器来尝试加载该类,如果应用程序类加载器不能加载成功,MyClassLoader才会自己尝试加载。

public class MyClass {
    static {
        System.out.println("MyClass is loaded by " + MyClass.class.getClassLoader());
    }
}
public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (!"MyClass".equals(name)) {
            // 如果不是MyClass类,则使用系统类加载器进行加载
            return super.loadClass(name);
        }

        String fileName = name + ".class";
        InputStream is = this.getClass().getResourceAsStream(fileName);
        if (is == null) {
            // 若找不到该类文件,先让系统类加载器尝试加载
            return super.loadClass(name);
        }

        try {
            byte[] bytes = new byte[is.available()];
            is.read(bytes);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        MyClassLoader loader = new MyClassLoader();
        // 使用MyClassLoader加载MyClass类
        Class<?> clazz = loader.loadClass("MyClass");
        Object instance = clazz.newInstance();
        System.out.println(instance.getClass().getClassLoader());
    }
}

运行结果:

MyClass is loaded by MyClassLoader@3fee733d
MyClassLoader@3fee733d

可以看到,MyClassLoader成功地加载了MyClass类,并打印了加载该类的类加载器。另外,由于MyClassLoader重写了loadClass方法,并在该方法中调用了父类ClassLoader的loadClass方法,因此程序先是通过MyClassLoader来尝试加载MyClass类,由于父类查找失败,于是再次调用MyClassLoader的findClass方法来加载MyClass类。

示例2:JVM自带的类加载器

JVM内置了三种类加载器:启动类加载器、扩展类加载器、应用程序类加载器。其中启动类加载器是JVM实现的一部分,它负责加载Java核心类库。扩展类加载器和应用程序类加载器都是Java类,因此用户可以根据需要自定义这两个类加载器的行为和策略。

public class Main {
    public static void main(String[] args) {
        // 查看各种类加载器加载的类的情况
        ClassLoader cl = Main.class.getClassLoader();
        System.out.println("Main类加载器: " + cl.toString());
        System.out.println("Main的父类加载器: " + cl.getParent().toString());
        System.out.println("引导类加载器: " + cl.getParent().getParent().toString());
        System.out.println("--------------------");

        cl = int.class.getClassLoader();
        System.out.println("int类加载器: " + cl.toString());
        System.out.println("int的父类加载器: " + cl.getParent().toString());
        System.out.println("引导类加载器: " + cl.getParent().getParent().toString());
        System.out.println("--------------------");

        cl = String.class.getClassLoader();
        System.out.println("String类加载器: " + cl.toString());
        System.out.println("String的父类加载器: " + cl.getParent().toString());
        System.out.println("引导类加载器: " + cl.getParent().getParent().toString());
    }
}

运行结果:

Main类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@58372a00
Main的父类加载器: jdk.internal.loader.ClassLoaders$PlatformClassLoader@5fd0d5ae
引导类加载器: null
--------------------
int类加载器: null
java.lang.NullPointerException
    String的父类加载器: null
java.lang.NullPointerException
    引导类加载器: null
--------------------
String类加载器: null
java.lang.NullPointerException
    String的父类加载器: null
java.lang.NullPointerException
    引导类加载器: null

可以看到,基本数据类型(如int)没有类加载器,而String类型是由引导类加载器加载的,也就是说,String类是由JVM实现的一部分,不是由Java代码写出来的。

总结

Java类加载器实现了JVM的基础功能,根据不同的需求可以使用JVM自带的类加载器或自定义类加载器。每个类加载器在加载类时,都会先委托其父类加载器完成,其它时候通过自定义的方式实现类的加载和查找。熟悉Java类加载器间的协作关系和实现方式,有助于编写出更灵活的Java应用程序。

本文链接:https://my.lmcjl.com/post/17167.html

展开阅读全文

4 评论

留下您的评论.