JVM-打破双亲委派机制-自定义类加载器

有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top

全网最细面试题手册,支持艾宾浩斯记忆法。这是一份最全面、最详细、最高质量的 java 面试题,不建议你死记硬背,只要每天复习一遍,有个大概印象就行了。 https://store.amazingmemo.com/chapterDetail/1685324709017001


1. 什么是 Java 类加载器?

Java 类加载器就是将 Java 字节码文件转换成 Java 类的一种机制。Java 虚拟机会在需要使用某个类时通过类加载器将该类加载进内存并转换成对应的 Java 类。

Java 类加载器主要有三类:Bootstrap ClassLoader、Extension ClassLoader 和 Application ClassLoader,其中 Bootstrap ClassLoader 是由 C++ 语言实现的,其他两个是由 Java 语言实现的。

2. 为什么需要自定义类加载器?

Java 类加载器的作用在于加载 Java 类,但是有时候默认的类加载器无法满足我们的需求。例如:

  • 在程序的运行时,可能需要从网络上下载一些 Java 类库并加载到 JVM 中,这时就需要自定义类加载器。

  • 为了实现不同的功能,可能需要区分不同的 Java 包,而默认类加载器无法实现这个功能,需要使用自定义类加载器进行区分。

3. 自定义类加载器的实现原理

自定义类加载器打破的是 Java 的双亲委派机制。Java 类加载器的加载顺序是从下往上走的,即:先由自定义类加载器加载类,如果找不到,再由父加载器加载,如果父加载器也找不到,再由祖先加载器加载,一直到 Bootstrap ClassLoader。这个机制可以保证同一个应用程序中的同一个类只被加载一次。

根据上述机制,我们实现自定义类加载器需要实现以下两个方法:

  • findClass() 方法: 如果默认加载器找不到类,则调用此方法来查找并加载类。

  • defineClass() 方法:将字节码转换成 Java 类并返回。

具体实现方式可见以下代码:

public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath, ClassLoader parent) {
        super(parent);
        this.classPath = classPath;
    }

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

    private byte[] getClassData(String className) {
        String path = classNameToPath(className);
        try {
            InputStream inputStream = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int num = 0;
            while ((num = inputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, num);
            }
            inputStream.close();
            return baos.toByteArray();
        } catch (Exception e) {
            return null;
        }
    }

    private String classNameToPath(String className) {
        return classPath + className.replace(".", "/") + ".class";
    }
}

该代码模拟了一个自定义类加载器。在这个例子中,我们在 findClass() 方法中使用 getClassData() 方法来加载类,并在 defineClass() 方法中将字节码转换成 Java 类并返回。

上述示例是一个简单的自定义类加载器示例,其中还有一些细节和注意事项需要引起我们的注意,具体会在后面的章节中提到。

4. 自定义类加载器的使用示例

在项目实际中,我们可能需要使用多个自定义类加载器来加载不同的类,这需要我们自己来实现 ClassLoader。下面是一个示例:

public class Test {
    public static void main(String[] args) {
        String classPath = "F:/work/TestDemo/bin/";
        MyClassLoader classLoader1 = new MyClassLoader(classPath, ClassLoader.getSystemClassLoader().getParent());
        MyClassLoader classLoader2 = new MyClassLoader(classPath, ClassLoader.getSystemClassLoader().getParent());
        try {
            Class<?> class1 = classLoader1.loadClass("demo.bean.User");
            Class<?> class2 = classLoader2.loadClass("demo.bean.User");
            System.out.println(class1 == class2); // false,因为类加载器不一样

            Object obj1 = class1.newInstance();
            Object obj2 = class2.newInstance();

            Method method = class1.getMethod("setUname", String.class);
            method.invoke(obj1, "张三");
            method.invoke(obj2, "李四");

            Method method1 = class1.getMethod("getUname");
            Method method2 = class2.getMethod("getUname");

            System.out.println(method1.invoke(obj1).equals(method2.invoke(obj2)));

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们使用了两个自定义类加载器(classLoader1 和 classLoader2),用它们分别加载了同样的一个类(demo.bean.User)。由于是使用不同的类加载器类加载同样的一个类,所以两个 class 对象是不相等的。我们在 setUname() 方法中设置了 obj1 中的 uName 为“张三”,在 obj2 中设置为“李四”,并在 getUname() 方法中返回了 uName 属性的值,在最终的输出中我们可以看到它们的返回值是不相等的。

5. 自定义类加载器的优点

  • 解决了类的相互依赖关系。由于自定义类加载器不受双亲委派机制的限制,所以我们可以通过自定义类加载器来避免由于类互相依赖而引起的加载问题。

  • 可以实现类隔离。自定义类加载器可以使用不同的命名空间,从而实现类隔离。

  • 扩展了 Java 类加载机制的实现。自定义类加载器可以通过继承 JDK 中已有的类来扩展类加载机制的实现,从而满足各种特定的需求。

6. 自定义类加载器的缺点

  • 安全问题。由于自定义类加载器打破了 Java 的双亲委派机制,所以可能导致外部加载非法类、内存泄漏等问题。

  • 代码复杂度高。相比于默认加载器的方式,使用自定义类加载器需要编写更多的代码,且需要考虑多种异常情况,如类的加载失败、类库的损坏等。

7. 自定义类加载器的使用注意事项

  • 由于自定义类加载器需要重写父类的 loadClass() 方法,所以我们需要注意自定义类加载器的作用范围,避免影响到系统的正确使用。

  • 自定义类加载器所加载的类的命名一定不能与系统默认的类库相同,避免加载类库出错。可以通过使用命名空间来实现。

  • 在加载类时,应该保证加载同一个类的不同实例使用的是同一个 Class 对象,否则会出现 Class Cast 异常。这可以使用 findLoadedClass() 方法来实现。

8. 总结

Java 类加载器是将 Java 字节码文件转换成 Java 类的机制。Java 类加载器分为三种:Bootstrap ClassLoader、Extension ClassLoader 和 Application ClassLoader。

自定义类加载器可以解决一些默认类加载器无法解决的问题,如实现类隔离、解决类的相互依赖关系等。自定义类加载器打破 Java 的双亲委派机制,需要重写 findClass() 方法和 defineClass() 方法。

自定义类加载器有一些缺点,如安全问题、代码复杂度高等,使用时需要注意一些细节和注意事项。

最后更新于