类加载器概述
类加载器(ClassLoader)是 Java 虚拟机(JVM)的一部分,用于将类文件加载到内存中,并将其转换为 Class
对象。类加载器在 Java 中起着非常重要的作用,它控制着类的加载和命名空间的隔离。
Java 中的类加载器有一个非常重要的特点,即双亲委派机制(Parent Delegation Model)。这个机制是为了保证 Java 的安全性和稳定性,即同一个类在整个应用中只有一个定义。具体来说,当一个类加载器加载类时,它首先会将请求委派给其父加载器,只有当父加载器无法找到该类时,才会尝试自己加载类。
Java 中的常见类加载器
-
Bootstrap ClassLoader(引导类加载器):
- 它是 JVM 自带的类加载器,用于加载 Java 核心库中的类,如java.lang.String
和其他基本类。
- 它是用本地代码实现的,并不是 Java 类的一部分,因此无法直接获取它的实例。 -
Extension ClassLoader(扩展类加载器):
- 这个类加载器用于加载位于JAVA_HOME/lib/ext
目录中的类,或者通过系统属性java.ext.dirs
指定的扩展目录中的类。
- 它是java.net.URLClassLoader
的一个实例。 -
Application ClassLoader(应用程序类加载器):
- 这是默认的类加载器,用于加载应用程序的类路径(classpath)中的类。
- 它也是java.net.URLClassLoader
的一个实例,并且是最常见的类加载器。 -
自定义类加载器:
- 开发者可以通过继承java.lang.ClassLoader
来创建自己的类加载器,实现特殊的类加载逻辑。通常用于加载外部资源或实现模块化等功能。
双亲委派机制
双亲委派机制的工作流程如下:
- 当应用程序请求加载某个类时,类加载器首先将该请求委派给其父类加载器。
- 父类加载器再将请求委派给它的父类加载器,最终请求会传递到 Bootstrap ClassLoader。
- 如果 Bootstrap ClassLoader 找不到这个类,则会逐级返回给下层的类加载器,直到原始请求的类加载器。
- 如果所有的父类加载器都找不到该类,则最终由请求的类加载器尝试加载该类。
这种机制确保了核心类库不会被替换或伪造,例如 java.lang.String
类总是由 Bootstrap ClassLoader 加载。
打破双亲委派机制
虽然双亲委派机制是 Java 类加载的默认行为,但在某些特殊情况下,我们可能需要打破这种机制。例如,开发者可能希望在不同的模块中加载具有相同类名但实现不同的类,或者实现某种热部署机制(比如在运行时替换类)。
打破双亲委派机制的方式主要有以下几种:
1. 自定义类加载器覆盖 loadClass
方法
默认情况下,ClassLoader
类的 loadClass
方法实现了双亲委派机制。如果想要打破这个机制,可以自定义一个类加载器并覆盖 loadClass
方法,不调用父类的 loadClass
,直接尝试加载类。例如:

public class CustomClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 不调用 super.loadClass(name),跳过双亲委派机制
try {
String fileName = name.replace('.', '/') + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
}
在这个自定义类加载器中,我们直接读取类文件并将其定义为一个 Class
对象,而没有按照双亲委派机制将请求委派给父类加载器。
2. OSGi 和模块化框架
像 OSGi 这样的模块化框架通过自定义类加载器和复杂的类加载规则来实现模块隔离和动态加载。这些框架通常不会遵循严格的双亲委派机制,而是根据模块的依赖关系和配置文件来决定如何加载类。
3. 使用 ThreadContextClassLoader
在 Java 应用程序中,有时通过设置线程上下文类加载器(ThreadContextClassLoader
)来加载类。这个类加载器通常与应用的特定上下文相关联,可以在不同的类加载器之间切换,从而绕过双亲委派机制。
Thread.currentThread().setContextClassLoader(customClassLoader);
通过设置 ThreadContextClassLoader
,应用可以在当前线程上下文中使用自定义类加载器加载类,而不是使用默认的类加载器。
4. 使用 Class.forName
的特定版本
Class.forName
方法可以加载类,并允许指定类加载器。例如:
Class<?> clazz = Class.forName("com.example.SomeClass", true, customClassLoader);
这种方式允许开发者指定一个类加载器来加载类,而不是使用默认的双亲委派机制。
双亲委派机制的意义与应用
双亲委派机制的主要目的是为了避免类加载的重复和类的多版本问题,确保 Java 核心类库的安全性。例如,在标准的类加载器实现中,java.lang.String
类始终由 Bootstrap ClassLoader 加载,这防止了应用程序自定义类加载器加载恶意或错误的 java.lang.String
类。
尽管如此,在一些复杂应用场景中,打破双亲委派机制是必要的。例如:
- 插件系统:许多应用程序支持通过插件的方式扩展功能。插件可能需要加载与核心应用不同的类实现,通常会通过自定义类加载器实现。
- 动态代理:一些框架(如 Spring)在运行时生成代理类,这些代理类需要使用自定义类加载器加载,以确保与应用的其他类兼容。
- 热部署:在 Web 容器或应用服务器中,热部署通常要求能够重新加载类,这可能需要打破双亲委派机制来加载新的类定义。
总结
类加载器是 Java 中非常重要的机制,它不仅控制类的加载过程,还决定了类在内存中的命名空间。双亲委派机制是类加载器的重要特性,确保了核心类库的一致性和安全性。然而,在某些情况下,出于功能扩展、热部署等需求,我们可能需要打破双亲委派机制。通过自定义类加载器或使用特定的类加载方法,开发者可以实现更灵活的类加载策略,满足复杂应用的需求。