【面试题精讲】JVM-打破双亲委派机制-自定义类加载器

简介: 【面试题精讲】JVM-打破双亲委派机制-自定义类加载器

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() 方法。

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

本文由 mdnice 多平台发布


相关文章
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
1262 55
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
664 35
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
Arthas Java 测试技术
JVM深入原理(六)(一):JVM类加载器
目录6. JVM类加载器6.1. 类加载器-概述6.2. 类加载器-执行流程6.3. 类加载器-分类(JDK8)6.3.1. JVM底层实现的类加载器6.3.1.1. 启动类加载器6.3.2. Java代码实现类的加载器6.3.2.1. 扩展类加载器6.3.2.2. 应用程序类加载器6.4. 类加载器-Arthas查看类加载器
336 0
|
Java 关系型数据库 MySQL
JVM深入原理(六)(二):双亲委派机制
自定义类加载器打破双亲委派机制的方法:复写ClassLoader中的loadClass方法常见问题:要加载的类名如果是以java.开头,则会抛出安全性异常加载自定义的类都会有一个共同的父类Object,需要在代码中交由父类加载器去加载自定义类加载器不手动指定parent会默认指定应用类加载两个自定义类加载器加载同一个类会被认为是两个对象,只有相同的类加载器+想通的类限定名才会被认为是一个对象。
555 0
|
Java 数据库连接 Maven
最新版 | SpringBoot3如何自定义starter(面试常考)
在Spring Boot中,starter是一种特殊的依赖,帮助开发人员快速引入和配置特定功能模块。自定义starter可以封装一组特定功能的依赖和配置,简化项目中的功能引入。其主要优点包括模块化、简化配置、提高代码复用性和实现特定功能。常见的应用场景有短信发送模块、AOP日志切面、分布式ID生成等。通过创建autoconfigure和starter两个Maven工程,并编写自动配置类及必要的配置文件,可以实现一个自定义starter。最后在测试项目中验证其有效性。这种方式使开发者能够更便捷地管理和维护代码,提升开发效率。
2426 1
最新版 | SpringBoot3如何自定义starter(面试常考)
|
SQL 缓存 监控
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
本文详细解析了数据库、缓存、异步处理和Web性能优化四大策略,系统性能优化必知必备,大厂面试高频。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:4 大性能优化策略(数据库、SQL、JVM等)
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
1167 3
|
前端开发 Java 应用服务中间件
JVM进阶调优系列(1)类加载器原理一文讲透
本文详细介绍了JVM类加载机制。首先解释了类加载器的概念及其工作原理,接着阐述了四种类型的类加载器:启动类加载器、扩展类加载器、应用类加载器及用户自定义类加载器。文中重点讲解了双亲委派机制,包括其优点和缺点,并探讨了打破这一机制的方法。最后,通过Tomcat的实际应用示例,展示了如何通过自定义类加载器打破双亲委派机制,实现应用间的隔离。
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。