JVM 类加载器有哪些?双亲委派机制的作用是什么?如何自定义类加载器?

开发 前端
引导类加载器 BootstrapClassLoader:引导类加载器是使用 C++ 语言实现的,嵌入在 JVM 中。用于加载 Java 中的核心类库的,不继承自 java.lang.ClassLoader,在 Java 程序中通常返回 null。

类加载器分类

先回顾下,在 Java 中,类的初始化分为几个阶段: 加载、链接(包括验证、准备和解析)和 初始化。

而 类加载器(Class Loader)则是加载阶段中,负责将本地或网络中的指定类的二进制流,加载到 Java 虚拟机中的工具。

图片图片

引导类加载器 BootstrapClassLoader

引导类加载器 BootstrapClassLoader:引导类加载器是使用 C++ 语言实现的,嵌入在 JVM 中。用于加载 Java 中的核心类库的,不继承自 java.lang.ClassLoader,在 Java 程序中通常返回 null。

一般会加载 JAVA_HOME 目录下的 /jre/lib 文件夹下的 jar 和配置。

ClassLoader loader = String.class.getClassLoader();
System.out.println(loader); // 输出 null,因为 String 是由引导类加载器加载的

扩展类加载器 ExtClassLoader

扩展类加载器主要负责加载 Java 的扩展类库,一般会加载 JAVA_HOME 目录下的 /jre/lib/ext 文件夹下的 jar。

继承自 java.lang.ClassLoader,是用户可以访问的第一个类加载器。

ClassLoader extLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println(extLoader); // 输出 sun.misc.Launcher$ExtClassLoader

应用类加载器(Application ClassLoader)

应用类加载器是应用程序中默认的类加载器,可以加载 CLASSPATH 变量指定目录下的 jar,由 sun.misc.Launcher$AppClassLoader 实现。

并且一般情况下,我们编写的 Java 应用的类,都是使用该类加载器完成加载的。

ClassLoader appLoader = ClassLoader.getSystemClassLoader();
System.out.println(appLoader); // 输出 sun.misc.Launcher$AppClassLoader

类加载器抽象类 ClassLoader

在 Java 中存在一个类加载器抽象类 ClassLoader,大多数类加载器都是通过继承这个类来实现的类加载功能。以下是 ClassLoader 类的关键部分代码:

public abstract class ClassLoader {

    /*
     * 类加载器的父加载器
     */
    private final ClassLoader parent;

    /**
     * 根据类的全限定名加载类
     *
     * @param name 类名称
     * @return     加载的Class对象
     * @throws ClassNotFoundException 没有发现指定类异常
     */
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 调用loadClass方法加载类,其中设置resolve=false,表示不立即解析类
        return loadClass(name, false);
    }

    /**
     * 根据类的全限定名加载类
     *
     * @param name    类名称
     * @param resolve 是否解析这个类,true=解析,false=不解析
     * @return 加载的Class对象
     * @throws ClassNotFoundException 没有发现指定类异常
     */
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            // 如果没有加载过
            if (c == null) {
                // 如果有父类加载器,则委托给父加载器去加载
                // 如果没有父类加载器,则判断 Bootstrap 类加载器是否加载过
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
                // 如果父类加载器都加载失败,则当前类加载器尝试自行加载
                if (c == null) {
                    c = findClass(name);
                }
            }
            // 据 resolve 参数决定是否解析类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    /**
     * 查找并加载指定名称的类
     *
     * @param name 类名称
     * @return Class对象
     * @throws ClassNotFoundException 没有发现指定类异常
     */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //1. 根据传入的类名,到在特定目录下去寻找类文件,把字节码文件读入内存
        // ...
        //2. 调用 defineClass 将字节数组转成 Class 对象
        return defineClass(buf, off, len);
    }

    /**
     * 将一个 byte[] 转换为 Class 类的实例
     *
     * @param name 类名称,如果不知道此名称,则该参数为 null
     * @param b    组成类数据的字节数组
     * @param off  类数据的起始偏移量
     * @param len  类数据的长度
     * @return Class对象
     * @throws ClassFormatError 类格式化异常
     */
    protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError {
        ...
    }

}

类中定义的常用的类加载相关的方法:

方法名称

描述

getParent()

返回该类加载器的父类加载器

loadClass(String name)

加载指定名称的类,返回 java.lang.Class 实例

findClass(String name)

查找指定名称的类,返回 java.lang.Class 实例

findLoadedClass(String name)

查找已加载的指定名称的类,返回 java.lang.Class 实例

defineClass(String name, byte[] b, int off, int len)

将字节数组转换为一个 Java 类,返回 java.lang.Class 实例

resolveClass(Class c)

连接指定的 Java 类

双亲委派模型(Parent Delegation Model)

双亲委派模型 是类加载器的设计模式,其核心思想是:类加载请求由子类加载器向父类加载器逐层委派,直到引导类加载器。

如果父类加载器无法加载,子类加载器才会尝试加载。

如果子类加载器也无法加载该类,就会抛出一个 ClassNotFoundException 异常。

图片图片

双亲委派机制的作用

我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的 String 类来动态替代 Java 核心 API 中定义的类型,这样会存在非常大的安全隐患。

而双亲委托的方式,就可以避免这种情况,因为 String 已经在启动时就被引导类加载器 (BootstrcpClassLoader) 加载,所以用户自定义的 ClassLoader 永远也无法加载一个用户自己自定义的 String 类,除非你改变 JDK 中 ClassLoader 搜索类的默认算法。

该机制的作用如下。

  • 防止重复加载字节码文件: 将类加载请求先委托给父类,父类加载后子类就不会重复加载该类。所以,双亲委派机制可以防止对某个类重复加载;
  • 防止核心字节码文件被篡改: 一般情况下引导类加载器会先加载 JVM 核心类库,然后其它加载器才会执行,如果其它加载器要加载一个被篡改的核心字节码文件,会将该文件委托给父类加载器,当委托到引导类加载器时,加载器已经加载过该类,就不会对该类进行重复加载。而且就算能被加载,那么加载它的肯定不是相同的类加载器 (不会是引导类加载器),Java 虚拟机中只认可核心类加载器加载的核心类库,所以,双亲委派机制可以防止核心字节码文件被篡改。
  • 简化加载逻辑: 通过委派模式,每个类加载器只需要关注自己负责的那部分类加载逻辑,而不必关心其他类加载器的加载细节,简化了类加载器的实现,降低了系统的复杂度。

自定义类加载器

在某些场景下,标准的类加载器无法满足需求,例如:

  1. 热部署:在 Web 服务器中动态加载或更新类。
  2. 模块隔离:在同一个 JVM 中加载不同版本的类。
  3. 加密解密:加载经过加密的 Class 文件。

默认的类加载器只能加载指定目录下的 Jar 和 Class 文件。

如果需要加载指定位置的类文件并实现一些自定义逻辑,就需要自定义类加载器。

Chaya:如何实现自定义类加载器?

步骤:

  • 继承 java.lang.ClassLoader 类。
  • 重写 findClass() 方法,通过字节流读取 Class 文件并转换为 Class 对象。
import java.io.*;

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

    private byte[] loadClassData(String name) {
        String fileName = name.replace('.', '/') + ".class";
        try (InputStream is = new FileInputStream(fileName);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int buffer;
            while ((buffer = is.read()) != -1) {
                baos.write(buffer);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

示例说明

  • findClass():从文件系统加载 Class 文件,并将其定义为 Class 对象。
  • defineClass():将字节数组转换为 JVM 可执行的 Class 对象。

为了为保证类加载器都正确实现双亲委派机制,在开发自己的类加载器时,只需要重写 findClass() 方法即可。

当然,如果不想使用双亲委派机制时,就需要重写 loadClass() 方法。

打破双亲委派模型

有时为了实现特殊功能,我们需要打破双亲委派模型,例如:

  • 热部署框架:Tomcat、Spring Boot 使用自定义类加载器加载和卸载 Web 应用。
  • SPI(Service Provider Interface)机制:JDBC 驱动等需要通过 线程上下文类加载器 来加载用户实现的接口。
责任编辑:武晓燕 来源: 码哥跳动
相关推荐

2024-04-09 08:41:41

JVM类加载Java

2023-12-06 12:11:43

类加载器双亲委派模型

2024-03-12 07:44:53

JVM双亲委托机制类加载器

2024-03-27 09:15:27

2020-11-06 00:50:16

JavaClassLoaderJVM

2021-07-05 06:51:43

Java机制类加载器

2022-08-08 08:17:43

类隔离加载器自定义类

2023-10-19 09:14:34

Java开发

2023-10-31 16:00:51

类加载机制Java

2020-10-26 11:20:04

jvm类加载Java

2024-12-02 09:01:23

Java虚拟机内存

2012-02-09 10:31:17

Java

2021-01-06 09:51:19

类加载器双亲委派模型

2021-04-29 11:18:14

JVM加载机制

2017-03-08 10:30:43

JVMJava加载机制

2017-09-20 08:07:32

java加载机制

2023-10-30 01:02:56

Java类类加载器双亲委派

2024-03-08 08:26:25

类的加载Class文件Java

2023-08-02 08:38:27

JVM加载机制

2024-06-24 14:52:50

Android类加载器
点赞
收藏

51CTO技术栈公众号