Java类加载器ClassLoader总结

本文深入探讨Java类加载器的工作原理,包括引导类加载器、扩展类加载器和系统类加载器的角色,以及类加载器的双亲委派模式如何确保Java核心库类型的统一性和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.类加载器概述

java类的加载由虚拟机来完成的,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化最终形成可以被虚拟机直接说那个的java类型,这就是虚拟机的类加载机制。

2.类的生命周期

在这里插入图片描述

3.类加载器分类

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
在这里插入图片描述
1.引导类加载器(Bootstrap ClassLoader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。负责加载 JAVA_HOME\lib 目录中的类
2.扩展类加载器(Extension ClassLoader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。负责加载 JAVA_HOME\lib\ext 目录中的
3.系统类加载器(System ClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

4.java.lang.ClassLoader类:

上面除了引导类加载器,其他的加载器都继承ClassLoader类
作用:
1.基本职责负责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例
2.ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等
相关方法:
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c) 链接指定的 Java 类。

注释:
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()方法就可以获取到此引用,也可以通过递归调用getParent()方法来输出全部的父类加载器

5.类加载器的代理模式(双亲委派模式):

工作原理:
如果一个类收到了加载类的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次类推。
请求最终到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就返回成功。
若父类加载器无法完成加载任务,子加载器才会尝试自己去加载。

优点:
保证类java核心库的类型安全。所有java应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到java虚拟机中。
若这个加载过程由自己的类加载器加载的话,就很可能存在多个版本的java.lang.Object类,而这些类之间是不兼容的。但是通过代理模式,对于java核心库的类加载工作由引导类加载器统一完成,保证了java应用所使用的都是同一个版本的java核心库的类,是相互兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。
相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间

需要说明的是:
Java 虚拟机是如何判定两个 Java 类是相同的。
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。
即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

6.线程上下文类加载器:

线程上下文类加载器(Context ClassLoader)
类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

上述的类加载器不能解决java应用开发的全部问题,比如java提供了很多服务提供者接口(SPI),允许第三方提供接口实现,最常见的是JDBC,java.sql.Driver这些接口由java核心库提供
所以这些类由启动类加载器加载,但是这些接口的实现通过jar包引入到项目,由系统类加载器加载。引导类加载器无法找到SPI的实现类,也不能代理给系统类加载器。这也是代理模式无法解决的问题。
但是线程上下文类加载器正好解决了这个问题。若不做任何设置,java应用的线程的上下文类加载器默认就是系统上下文类加载器。在SPI接口的代码中使用上下文类加载器就可以成功的加载到SPI接口的实现类。

7.测试案例

本案例主要测试,使用不同的类加载器加载相同类的.class文件,并得到两个java.lang.Class类的实例,但是这两个实例是不相同的。
自定义类加载器:

public class MyClassLoader2 extends ClassLoader {

    private String rootDir;

    public MyClassLoader2(String path) {
        this.rootDir = path;
    }

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

    private byte[] getClassData(String className) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        String path = classNameToPath(className);
        try {
            InputStream is = new FileInputStream(path);
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = is.read(bytes)) != -1) {
                baos.write(bytes, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return baos.toByteArray();
    }

    private String classNameToPath(String className) {
        // File.separatorChar 与系统有关的默认名称分隔符
        return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    }
}

加载流程:

一般来说,写自己的类加载器只需要重写父类的findClass(String name)方法即可,因为loadClass(String name)方法中封装了双亲委派模式的实现,该方法中①首先就会调用findLoadedClass(String name)方法,检查这个类是否被加载过,②若被加载过,直接返回该类的字节码对象,③若没有加载过,会先调用父类的loadClass()方法,尝试加载该类,④如果父类无法加载,就调用findClass(String name)查找该类。因此为了正常的类加载流程,最好重写findClass(String name)方法

测试代码及结果:
在这里插入图片描述

这里需要注意的是,如果在IDEA里面直接创建Student类的话,不会出现这种结果,因为系统类加载器会加载到Student而不是自己的类加载器去加载。所以这里我把Student类放在了非classpath目录下:
在这里插入图片描述
这里还需要注意一个问题就是,不能再IDEA创建好之后,在从其classpath下拿出.class文件直接放在这个目录下,因为.java文件在编译成.class之后类的描述信息已经存在.class文件中了,比如包名,如果直接copy到这个目录下,会出现类名称错误的异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值