一、类加载器
1. 作用
实现 通过一个类的全限定名来获取描述该类的二进制字节流 动作,即类的加载动作。
在虚拟机中,每个类加载器都有一个独立的类名称空间,故只有在 两个类的类的全限定名相同,且加载该类的加载器相同 的情况下,才判定相等(包括 equals()
、isAssignableFrom()
、isInstance()
方法及 instanceOf
关键字的判断结果)。
2. 分类
启动类加载器 (Bootstrap Class Loader)
负责加载存放在 <JAVA_HOME>\lib
目录,或者被 -Xbootclasspath 参数所指定的路径中存放的,且文件名能被识别的类库(如 rt.jar
、tools.jar
,文件名不符合目录正确也不会被加载)加载到 JVM 内存中。此加载器无法被 Java 程序直接使用,自定义类加载器若需要委派加载请求给此加载器加载,直接使用 null 代替即可。
扩展类加载器(Extension Class Loader)
负责加载 <JAVA_HOME>\lib\ext
目录中,或者被 java.ext.dirs
系统变量所指定路径中的所有类库。此类库中存放具有通用性的扩展类库,且允许用户自行添加,即扩展机制。在类 sun.misc.Launcher$ExtClassLoader
中以 Java 代码形式实现,故用户可直接在程序中使用此类加载器加载 Class 文件。JDK 9 中,此扩展类加载器被平台类加载器替代。
平台类加载器(Platform Class Loader)
由于模块化系统中,整个 JDK 都基于模块化构建,故Java 类库为满足模块化需求,未保留 <JAVA_HOME>\lib\ext
目录,扩展类加载器也被替换为平台类加载器。
应用程序类加载器(Application Class Loader)
负责加载用户类路径(ClassPath)上所有类库,开发者可直接使用此类加载器。由于此加载器在 ClassLoader 类中是方法getSystemClassLoader()
的返回值,故又称系统类加载器。若用户未自定义加载器,一般情况下为默认加载器。
自定义类加载器
可通过重写 ClassLoader 类的 findClass()
方法实现自定义类加载器,以完成某些功能。
二、双亲委派模型
1. 描述
如果一个类加载器收到了类加载的请求,它不会加载自己尝试加载此类,而是委派请求给父类加载器进行加载。
2. 意义
共享
使 Java 类随着它的类加载器一起具备了一种 带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父类加载器已经加载了该类时,子 ClassLoader 就没有必要再加载一次。
隔离
隔离功能,保证核心类库的纯净和安全,防止恶意加载,避免了 Java 的核心 API 被篡改。
保证唯一
若不采用双亲委派机制,同一个类有可能被多个类加载器加载,这样该类会被识别为两个不同的类。
双亲委派机制在很大程度上防止内存中出现多个相同的字节码文件,加载类的时候默认会使用当前类的 ClassLoader 进行加载,只有当你使用该 class 的时候才会去装载,一个加载器只会装载同一个 class 一次。
3. 源码
源码比较简单,全部集中在 java.lang.ClassLoader
的 loadClass()
方法中。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求类是否被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出 ClassNotFoundException,说明父类加载器无法完成加载请求
// from the non-null parent class loader
}
if (c == null) {
// 在父类加载器无法加载时,再调用本身的 findClass 方法进行类加载
long t1 = System.nanoTime();
c = findClass(name);
// 记录统级信息
...
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
先检查请求加载的类型是否已加载过,若没有则调用父加载器的 loadClass()
方法;若父类加载器为空则默认使用启动类作为父加载器。若父类加载器加载失败抛出 ClassNotFoundException
异常,才调用自己的 findClass()
方法尝试进行加载。
4. 双亲委派模型图
如图,即为 JDK 1.8 及以前的双亲委派模型图,除顶层类加载器外,其余类加载器都必须有自己的父类加载器。类加载器的父子关系一般 不以继承关系 实现,而是 组合关系 复用父加载器代码。
JDK 9 中因模块化的加入而重构了目录结构,也顺带将扩展类加载器替换为平台类加载器。虽然总体上仍保持三层类加载器和双亲委派架构,但委派关系发生变动。
当平台及应用程序类加载器收到类加载请求,在委派给父类加载器前,要先 判断是否归属于某一个系统模块,如果找到归属关系,则优先委派给对应模块的类加载器。由此,也可以算是类加载器的 第四次被破坏。
三、双亲委派模型的三次破坏
双亲委派模型仅仅是 Java 设计者推荐开发者们的类加载器实现方式,并不是强制约束的模型。截至目前,大多数 Java 圈的类加载器都遵循此模型,但仍出现过三次较大规模破坏。
1. 兼容 JDK 1.2 前的程序
产生原因
由于双亲委派模型是 JDK 1.2 才被引入,但 Java 第一个版本即存在抽象类 java.lang.ClassLoader
,开发者已编写好自定义类加载器,若进行 JDK 升级,则会导致 loadClass()
被覆盖,而正确做法应为重写 findClass()
。
解决方案
为了兼容已存在的用户自定义类,Java 设计者们只能在 JDK 1.2 后添加一个新的 protected
方法 findClass()
,并引导用户尽可能在类加载逻辑中重写此方法,而不是在 loadClass()
中编写代码。按照 loadClass()
方法逻辑,父类加载失败,会调用自己的 findClass()
方法完成加载,保证新代码也可以符号双亲委派规则。
2. 自身的缺陷
产生原因
双亲委派模型很好地解决了各个类加载器协作时基础类型一致性问题,即越基础的类由越上层的类加载器加载。但基础类型也会存在调用回用户代码的场景。
场景
典型的例子便是 JNDI 服务,此服务已是 Java 标准服务。它的代码由启动类加载器完成加载(即在 rt.jar
中),属于 Java 中很基础的类型。但 JNDI 存在的目的就是对资源进行查找和集中管理,故需要调用其他厂商实现并部署在应用程序 ClassPath 下的 JNDI 服务提供者接口(SPI),但启动类不可能识别且加载这些代码。
解决方案
为解决此问题,Java 设计团队引入了线程上下文类加载器。此加载器可通过 java.lang.Thread
类的 setContextClassLoader()
方法进行设置,如果创建线程时未设置,它将从父类继承一个,如果应用程序全局范围内都未设置,则这个类加载器默认为应用程序类加载器。故以此即可加载所需的 SPI 服务代码。此方式为一种父类加载器请求子类加载器完成类加载的行为。
3. 实现程序动态性
产生原因
程序动态性即代码热替换、模块热部署等功能。
场景
OSGi 是实现热部署的常用规范,其实现热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(在 OSGi 中称为 Bundle)都有一个类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码热替换。
实现方式
在 OSGi 环境下,类加载器不再是双亲委派模型推荐的树状结构,而是更加复杂的网状结构,它的委派关系仅少部分遵守双亲委派模型,其余部分会在平级类加载器中查找。