类加载器是 JVM 中一个非常重要的子系统,它负责将 Class 文件(包含类的字节码)加载到 JVM 的运行时数据区(主要是方法区)中,并在堆内存中创建对应的 java.lang.Class
对象,作为程序中访问这些类的入口。
类加载器的作用:
- 加载: 从不同的来源(如本地文件系统、JAR 包、网络等)读取 Class 文件的二进制字节流。
- 链接: 执行连接阶段的验证、准备和解析(解析是可选的)。
- 初始化: 执行类的初始化方法
<clinit>()
。
类加载器的层次结构和双亲委派模型:
为了更好地管理类加载过程,并保证 Java 核心库的稳定性和安全性,JVM 采用了双亲委派模型(Parent-First Delegation Model)。在这个模型中,类加载器之间存在一种层次关系,通常由以下几个主要的类加载器组成:
-
启动类加载器 (Bootstrap ClassLoader):
- 这是 JVM 最底层的类加载器,由 C++ 实现,是 JVM 自身的一部分。
- 它负责加载 JVM 启动时需要的核心类库,例如
java.lang.*
、java.util.*
等,这些类库通常位于<JAVA_HOME>/lib
目录下。 - 启动类加载器没有父类加载器。
-
扩展类加载器 (Extension ClassLoader):
- 由 Java 语言实现(
sun.misc.Launcher$ExtClassLoader
)。 - 它是启动类加载器的子类加载器。
- 它负责加载
<JAVA_HOME>/lib/ext
目录下的,或者被java.ext.dirs
系统变量所指定的路径下的所有 JAR 包中的类库。 - 它的父类加载器是启动类加载器。
- 由 Java 语言实现(
-
应用程序类加载器 (Application ClassLoader) 或系统类加载器 (System ClassLoader):
- 由 Java 语言实现(
sun.misc.Launcher$AppClassLoader
)。 - 它是扩展类加载器的子类加载器。
- 它负责加载用户在命令行中指定的 classpath 下的类库,或者是由
java.class.path
系统变量所指定的路径下的类库。 - 它是我们平时开发中最常用的类加载器,也是默认情况下加载应用程序中类的加载器。
- 它的父类加载器是扩展类加载器。
- 由 Java 语言实现(
除了以上三个主要的类加载器,用户还可以根据自己的需求创建自定义的类加载器 (User-Defined ClassLoader),通过继承 java.lang.ClassLoader
类并重写其 findClass()
方法来实现特定的类加载逻辑。
双亲委派模型的工作流程:
当一个类加载器收到加载一个类的请求时,它不会立即自己去加载,而是将这个请求委派给它的父类加载器,直到委派给最顶层的启动类加载器。只有当父类加载器在它的搜索范围内找不到所需的类时(即无法完成加载),子类加载器才会尝试自己去加载。
双亲委派模型的好处:
- 安全性: 避免用户自定义的恶意类替换 JVM 核心类库中的类。例如,即使你创建了一个名为
java.lang.String
的类,由于类加载请求首先会委派给启动类加载器,而启动类加载器会优先加载它自己负责的java.lang.String
,因此你的自定义类不会被加载。 - 避免类的重复加载: 确保一个类只会被加载一次,不同的类加载器加载同一个类最终会委托给同一个父加载器,如果父加载器已经加载过该类,则会直接返回已加载的
Class
对象。这有助于维护类的一致性。
破坏双亲委派模型的情况:
虽然双亲委派模型是 JVM 推荐的类加载机制,但在某些特殊情况下,为了满足特定的需求,可能会出现破坏双亲委派模型的情况:
- 线程上下文类加载器 (Thread Context ClassLoader): JNDI、JDBC 等 SPI (Service Provider Interface) 机制中会用到线程上下文类加载器。SPI 的接口通常由引导类加载器加载,但接口的实现类可能由应用程序类加载器加载。为了让引导类加载器能够加载到这些实现类,需要通过线程上下文类加载器向上委托给应用程序类加载器。
- 热部署和模块化: 在某些需要动态更新和替换模块的场景下,例如 OSGi 框架,可能会创建自定义的类加载器来加载和卸载模块,这可能会打破严格的双亲委派模型。
自定义类加载器:
用户可以通过继承 java.lang.ClassLoader
类并重写其 findClass(String name)
方法来实现自定义的类加载逻辑。在 findClass()
方法中,你需要根据类的全限定名,从特定的来源(例如加密过的 Class 文件、网络)读取类的字节码,并调用 defineClass(String name, byte[] b, int off, int len)
方法将字节码转换为 Class
对象。
使用自定义类加载器的场景:
- 加载特定来源的类文件: 例如,从网络、数据库或加密过的文件中加载类。
- 实现类的隔离: 不同的类加载器可以加载相同全限定名的类,从而实现类之间的隔离,避免命名冲突。
- 实现热部署: 通过创建新的类加载器加载新的类版本,并在需要时卸载旧的类加载器和类。
- 扩展 JVM 的类加载机制。
总结:
类加载器是 JVM 核心组件之一,负责将 Class 文件加载到 JVM 中。双亲委派模型是一种重要的类加载机制,它保证了 Java 核心库的安全性和类的唯一性。用户也可以通过自定义类加载器来扩展 JVM 的类加载能力,以满足特定的应用需求。理解类加载器的工作原理对于深入理解 Java 的运行时行为至关重要。