类加载器详解
类加载器核心概念
类加载器是负责加载类的对象,核心作用是将 Java 类的二进制字节流(.class 文件)动态加载到 JVM 中,在内存生成对应的 Class 对象作为访问入口。ClassLoader 是抽象类,除 BootstrapClassLoader 由 C++ 实现外,其余类加载器均由 Java 实现并继承自该类。每个类都持有加载它的类加载器引用,数组类由 JVM 自动创建,其类加载器与元素类型的类加载器一致(基本类型数组无类加载器)。类加载器遵循 "按需加载" 和 "相同二进制名称类仅加载一次" 规则,已加载的类会存储在自身的 classes 集合中。
JVM 内置类加载器
JVM 内置三种类加载器,按层级从高到低为:
- BootstrapClassLoader(启动类加载器):顶层加载器,C++ 实现,无父加载器,加载 JDK 核心类库(% JAVA_HOME%/lib 目录下的 rt.jar 等)及 -Xbootclasspath 指定路径的类,获取其引用时返回 null。
- ExtensionClassLoader(扩展类加载器):加载 % JRE_HOME%/lib/ext 目录及 java.ext.dirs 系统变量指定路径的类,父加载器为 BootstrapClassLoader。
- AppClassLoader(应用程序类加载器):面向用户,加载当前应用 classpath 下的类,父加载器为 ExtensionClassLoader,是用户自定义类的默认加载器。Java9 后扩展类加载器更名为平台类加载器,除 java.base 等核心模块由启动类加载器加载外,其他模块由其加载。
自定义类加载器
自定义类加载器需继承 ClassLoader 抽象类,推荐重写 findClass () 方法(不打破双亲委派模型),仅在父类加载器无法加载时,通过该方法自定义字节流获取逻辑(如解密加密的 .class 文件);若需打破双亲委派模型,则需重写 loadClass () 方法,改变 "先委派父类加载" 的默认流程。自定义类加载器的父加载器可通过构造函数指定,可是内置类加载器或其他自定义类加载器,通过组合关系复用父加载器逻辑。
双亲委派模型
(1)核心定义
双亲委派模型是 JDK 推荐的类加载机制,要求除顶层启动类加载器外,所有类加载器都有父加载器,类加载请求会先委派给父加载器执行,仅当父加载器无法加载时,子加载器才尝试自己加载。此处 "双亲" 并非指两个父加载器,而是层级委派关系,父子关系通过组合实现而非继承。
(2)执行流程
- 类加载时先检查当前类是否已加载,已加载则直接返回;
- 未加载则将请求委派给父加载器,最终传递至 BootstrapClassLoader;
- 父加载器尝试加载,若成功则返回 Class 对象,失败则子加载器调用 findClass () 方法自行加载;
- 若子加载器也无法加载,抛出 ClassNotFoundException 异常。
(3)核心好处
- 避免类重复加载:同一类由父加载器统一加载,确保类的唯一性;
- 保护核心 API:核心类(如 java.lang.Object)由 BootstrapClassLoader 加载,且 ClassLoader 的 preDefineClass () 方法禁止加载以 "java." 开头的类,防止恶意篡改核心类。
打破双亲委派模型的方式
双亲委派模型非强制性约束,打破方式主要有两种:
- 重写 loadClass () 方法:自定义类加载器重写该方法,改变 "先委派父加载" 的逻辑,例如 Tomcat 的 WebAppClassLoader,优先加载 Web 应用目录下的类,实现 Web 应用间类隔离;
- 利用线程上下文类加载器:通过 Thread 的 setContextClassLoader () 设置上下文类加载器,让高层类加载器(如加载 SPI 接口的 BootstrapClassLoader)借助低层类加载器(如加载 SPI 实现的 AppClassLoader)加载类,解决 SPI 接口与实现类加载层级不一致的问题,Spring 等框架常采用此方式加载业务类。