0%

类加载器分类

JVM 类加载器分类与双亲委派机制

类加载器(ClassLoader)是 JVM 实现类加载机制的核心组件,负责将不同来源的 .class 文件加载到内存中。JVM 中类加载器分为默认类加载器自定义类加载器,它们通过 “双亲委派机制” 协同工作,确保类加载的安全性和一致性。本文将详细解析各类加载器的特点、职责,以及双亲委派机制的原理与应用。

默认类加载器:JVM 内置的加载器

JVM 提供了三种默认类加载器,各自负责特定路径的类加载,形成层级关系(非继承,而是委托关系)。

引导类加载器(Bootstrap ClassLoader)

  • 实现语言:由 C/C++ 编写(非 Java 代码),是 JVM 自身的一部分。
  • 加载范围:负责加载 JDK 核心类库,具体包括:
    • JAVA_HOME/jre/lib 目录下的核心 jar 包(如 rt.jarresources.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
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClassLoaderDemo {
public static void main(String[] args) {
// 核心类(如Object)由引导类加载器加载(返回null)
System.out.println(Object.class.getClassLoader()); // null

// 扩展类(如javax.swing.JFrame)由扩展类加载器加载
System.out.println(javax.swing.JFrame.class.getClassLoader());
// 输出:sun.misc.Launcher$ExtClassLoader@xxxx

// 应用类由系统类加载器加载
System.out.println(ClassLoaderDemo.class.getClassLoader());
// 输出:sun.misc.Launcher$AppClassLoader@xxxx
}
}

自定义类加载器:灵活扩展加载逻辑

除默认加载器外,开发者可通过继承 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class CustomClassLoader extends ClassLoader {
private String classPath; // 类加载路径

public CustomClassLoader(String classPath) {
this.classPath = classPath;
}

// 重写findClass:自定义类查找逻辑
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 将类名转换为文件路径(如com.example.User → com/example/User.class)
String path = name.replace(".", "/") + ".class";
// 从指定路径读取字节码
byte[] classData = loadClassData(path);
// 将字节码解析为Class对象
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException("类加载失败:" + name, e);
}
}

// 读取类文件的字节数据
private byte[] loadClassData(String path) throws IOException {
try (InputStream is = new FileInputStream(classPath + "/" + path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}

public static void main(String[] args) throws Exception {
// 创建自定义类加载器(加载路径为./custom/classes)
CustomClassLoader loader = new CustomClassLoader("./custom/classes");
// 加载com.example.Test类
Class<?> clazz = loader.loadClass("com.example.Test");
// 反射执行方法
clazz.getMethod("sayHello").invoke(clazz.newInstance());
}
}

说明

  • 自定义加载器通过 findClass 实现从 ./custom/classes 目录加载类,父加载器默认是系统类加载器;
  • 加载流程遵循双亲委派:先委托父加载器(系统类加载器)加载,若父加载器未找到,再调用 findClass 加载。

双亲委派机制:类加载的安全保障

双亲委派机制是类加载器之间的协作规则,核心是 “先委托父加载器加载,父加载器无法加载时再自己尝试”,确保类加载的一致性和安全性。

核心原理

  1. 委托父加载:当一个类加载器收到加载请求时,首先将请求委托给其父加载器(而非自己直接加载)。
  2. 递归向上:父加载器重复委托过程,直到请求到达顶层的引导类加载器。
  3. 尝试加载:若父加载器能加载该类,则返回加载结果;若所有父加载器都无法加载,当前加载器才会尝试自己加载(调用 findClass 方法)。

流程图

源码体现(ClassLoader.loadClass 方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2. 若有父加载器,委托父加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 无父加载器(如扩展类加载器),委托引导类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载
}

if (c == null) {
// 4. 父加载器均无法加载,调用findClass自己加载
long t1 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c); // 链接类
}
return c;
}
}

核心作用

  • 避免类重复加载:同一类(全限定名相同)由同一个加载器加载,确保 JVM 中类的唯一性(类的唯一性由 “加载器 + 全限定名” 共同决定)。
  • 保护核心类安全:防止恶意类篡改 JDK 核心类(如自定义 java.lang.Object 类,会被引导类加载器优先加载核心 Object,避免替换)。
  • 层级协作:父加载器加载的类可被所有子加载器共享(如核心类 String 由引导类加载器加载,所有应用都可使用)。

局限性与打破双亲委派的场景

双亲委派机制的单向委托(子→父)导致顶层加载器无法访问底层加载器加载的类,某些场景下需要打破这一机制:

  • Web 容器隔离:如 Tomcat 需为每个 Web 应用创建独立类加载器,避免不同应用的类冲突(如同一类的不同版本)。Tomcat 采用 “当前类加载器优先” 策略,先尝试自己加载,再委托父加载器。
  • 热部署:通过自定义类加载器重新加载类(旧类加载器被回收,新类加载器加载更新后的类)。
  • SPI 机制:Java 的 SPI(如 JDBC)中,核心类(如 DriverManager)由引导类加载器加载,但需调用用户实现的驱动类(由系统类加载器加载),通过线程上下文类加载器(Thread.getContextClassLoader())打破委派限制。
Tomcat 类加载器结构(打破双亲委派的典型案例)

Tomcat 为实现 Web 应用隔离,自定义了多层类加载器:

  1. Bootstrap:引导类加载器(加载 JVM 核心类)。
  2. System:系统类加载器(加载 Tomcat 自身启动类)。
  3. Common:通用类加载器(加载 Tomcat 和所有应用共享的类,如 servlet-api.jar)。
  4. WebApp:应用类加载器(每个 Web 应用一个,加载 WEB-INF/classesWEB-INF/lib 下的类,优先自己加载,再委托父加载器)。

类加载器的其他关键细节

类加载器的 “父子关系”

  • 类加载器的 “父加载器” 通过 ClassLoader 类的 parent 字段维护(组合关系),而非继承关系。
  • 可通过 ClassLoader.getParent() 方法获取父加载器(引导类加载器的父加载器为 null)。

数组类的加载器

  • 数组类的类加载器与数组元素的类加载器一致(如 User[] 的加载器与 User 的加载器相同)。
  • 基本类型数组(如 int[])没有类加载器(int[].class.getClassLoader() 返回 null)。

类的相等性判断

两个类相等(Class.equals() 返回 true)的前提是:

  • 全限定名相同;
  • 由同一个类加载器加载。
    即使两个类的字节码完全相同,若由不同加载器加载,也会被视为不同的类

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10