JVM 类加载器分类与双亲委派机制
类加载器(ClassLoader)是 JVM 实现类加载机制的核心组件,负责将不同来源的 .class 文件加载到内存中。JVM 中类加载器分为默认类加载器和自定义类加载器,它们通过 “双亲委派机制” 协同工作,确保类加载的安全性和一致性。本文将详细解析各类加载器的特点、职责,以及双亲委派机制的原理与应用。
默认类加载器:JVM 内置的加载器
JVM 提供了三种默认类加载器,各自负责特定路径的类加载,形成层级关系(非继承,而是委托关系)。
引导类加载器(Bootstrap ClassLoader)
- 实现语言:由 C/C++ 编写(非 Java 代码),是 JVM 自身的一部分。
- 加载范围:负责加载 JDK 核心类库,具体包括:
JAVA_HOME/jre/lib目录下的核心 jar 包(如rt.jar、resources.jar等,名称符合 JVM 识别规则的类库);- 由系统属性
sun.boot.class.path指定的路径。
- 特点:
- 没有继承
java.lang.ClassLoader类(是 JVM 内置组件); - 是所有类加载器的 “根加载器”,其他加载器的 “父加载器” 默认指向它;
- 无法通过 Java 代码直接获取其实例(
Class.getClassLoader()对核心类返回null,如Object.class.getClassLoader()返回null)。
- 没有继承
扩展类加载器(Extension ClassLoader)
- 实现类:
sun.misc.Launcher$ExtClassLoader(Java 语言编写),继承java.lang.ClassLoader。 - 加载范围:负责加载 Java 扩展类库,具体包括:
JAVA_HOME/jre/lib/ext目录下的 jar 包;- 由系统属性
java.ext.dirs指定的路径。
- 父加载器:逻辑上的父加载器是引导类加载器(实际通过委托机制关联,而非继承)。
- 作用:扩展 JDK 功能,用户可将自定义扩展类放入
ext目录,由其自动加载。
系统类加载器(Application ClassLoader)
- 实现类:
sun.misc.Launcher$AppClassLoader(Java 语言编写),继承java.lang.ClassLoader。 - 加载范围:负责加载应用程序的类,具体包括:
- 由
classpath环境变量指定的路径(可通过-cp或-classpath命令行参数修改); - JAR 包中
Manifest文件的Class-Path属性指定的路径。
- 由
- 父加载器:扩展类加载器。
- 特点:
- 是程序默认的类加载器,应用中大多数类由它加载;
- 可通过
ClassLoader.getSystemClassLoader()方法获取其实例。
示例:验证类加载器的加载路径
1 | public class ClassLoaderDemo { |
自定义类加载器:灵活扩展加载逻辑
除默认加载器外,开发者可通过继承 java.lang.ClassLoader 实现自定义类加载器,满足特殊需求(如加载加密类、从网络加载类、实现应用隔离等)。
自定义类加载器的核心方法
自定义类加载器需重写以下关键方法(遵循双亲委派机制):
| 方法 | 作用 | 是否建议重写 |
|---|---|---|
findClass(String name) |
在父加载器加载失败后,根据自定义逻辑查找并加载类(如从特定路径读取字节码) | 是(核心方法,保证遵循双亲委派) |
defineClass(String name, byte[] b, int off, int len) |
将字节数组(byte[])解析为 JVM 可识别的 Class 对象(不可手动修改,由父类提供) |
否(直接调用父类实现) |
loadClass(String name) |
实现双亲委派的核心逻辑(先委托父加载器,失败后调用 findClass) |
否(除非需要打破双亲委派) |
resolveClass(Class<?> c) |
链接类(验证、准备、解析),确保类可执行 | 按需调用(默认不自动链接) |
自定义类加载器示例
以下是一个简单的自定义类加载器,从指定目录加载类:
1 | public class CustomClassLoader extends ClassLoader { |
说明:
- 自定义加载器通过
findClass实现从./custom/classes目录加载类,父加载器默认是系统类加载器; - 加载流程遵循双亲委派:先委托父加载器(系统类加载器)加载,若父加载器未找到,再调用
findClass加载。
双亲委派机制:类加载的安全保障
双亲委派机制是类加载器之间的协作规则,核心是 “先委托父加载器加载,父加载器无法加载时再自己尝试”,确保类加载的一致性和安全性。
核心原理
- 委托父加载:当一个类加载器收到加载请求时,首先将请求委托给其父加载器(而非自己直接加载)。
- 递归向上:父加载器重复委托过程,直到请求到达顶层的引导类加载器。
- 尝试加载:若父加载器能加载该类,则返回加载结果;若所有父加载器都无法加载,当前加载器才会尝试自己加载(调用
findClass方法)。
流程图:
源码体现(ClassLoader.loadClass 方法)
1 | public Class<?> loadClass(String name) throws ClassNotFoundException { |
核心作用
- 避免类重复加载:同一类(全限定名相同)由同一个加载器加载,确保 JVM 中类的唯一性(类的唯一性由 “加载器 + 全限定名” 共同决定)。
- 保护核心类安全:防止恶意类篡改 JDK 核心类(如自定义
java.lang.Object类,会被引导类加载器优先加载核心Object,避免替换)。 - 层级协作:父加载器加载的类可被所有子加载器共享(如核心类
String由引导类加载器加载,所有应用都可使用)。
局限性与打破双亲委派的场景
双亲委派机制的单向委托(子→父)导致顶层加载器无法访问底层加载器加载的类,某些场景下需要打破这一机制:
- Web 容器隔离:如 Tomcat 需为每个 Web 应用创建独立类加载器,避免不同应用的类冲突(如同一类的不同版本)。Tomcat 采用 “当前类加载器优先” 策略,先尝试自己加载,再委托父加载器。
- 热部署:通过自定义类加载器重新加载类(旧类加载器被回收,新类加载器加载更新后的类)。
- SPI 机制:Java 的 SPI(如 JDBC)中,核心类(如
DriverManager)由引导类加载器加载,但需调用用户实现的驱动类(由系统类加载器加载),通过线程上下文类加载器(Thread.getContextClassLoader())打破委派限制。
Tomcat 类加载器结构(打破双亲委派的典型案例)
Tomcat 为实现 Web 应用隔离,自定义了多层类加载器:
- Bootstrap:引导类加载器(加载 JVM 核心类)。
- System:系统类加载器(加载 Tomcat 自身启动类)。
- Common:通用类加载器(加载 Tomcat 和所有应用共享的类,如
servlet-api.jar)。 - WebApp:应用类加载器(每个 Web 应用一个,加载
WEB-INF/classes和WEB-INF/lib下的类,优先自己加载,再委托父加载器)。
类加载器的其他关键细节
类加载器的 “父子关系”
- 类加载器的 “父加载器” 通过
ClassLoader类的parent字段维护(组合关系),而非继承关系。 - 可通过
ClassLoader.getParent()方法获取父加载器(引导类加载器的父加载器为null)。
数组类的加载器
- 数组类的类加载器与数组元素的类加载器一致(如
User[]的加载器与User的加载器相同)。 - 基本类型数组(如
int[])没有类加载器(int[].class.getClassLoader()返回null)。
类的相等性判断
两个类相等(Class.equals() 返回 true)的前提是:
- 全限定名相同;
- 由同一个类加载器加载。
即使两个类的字节码完全相同,若由不同加载器加载,也会被视为不同的类

v1.3.10