Java 类加载机制 - 类加载器(ClassLoader)与双亲委派模型

Java 虚拟机类加载过程中的 “加载” 阶段第一步就是 “通过一个类的全限定名来获取描述此类的二级制字节流”,这个动作由 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的模块叫做 “类加载器”。

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在 Java 程序中的作用远不限于此。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。

换言之,比较两个类 “相等”,只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

Java 中的类加载器

Java 虚拟机只有两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):使用 C++ 语言(HotSpot)实现,是虚拟机的一部分,该类加载器实例无法被用户获取;
  • 所有其它的类加载器:均由 Java 语言实现,独立于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader;

从 Java 程序员的角度,类加载器还可以继续细化,绝大部分 Java 程序都会使用到以下 3 种类加载器。

  • 启动类加载器 (Bootstrap ClassLoader):这个类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的目录中的,并且是虚拟机识别的(仅按照文件名识别,例如 rt.jar)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义加载器时,如果需要把加载请求委托给引导类加载器,直接使用 null 代替即可。
  • 扩展类加载器(Extension ClassLoader):这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,他负责加载 <JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$AppClassLoader 实现。该类是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此也称作 “系统类加载器”。它负责用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序的默认类加载器。

应用程序一般由这 3 中类加载器相互配合加载,如果有必要,还可以加入自己定义的类加载器,集成。

自定义类加载器

自定义类加载器可以直接或间接继承自类 java.lang.ClassLoader。在 java.lang.ClassLoader 类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name) 方法即可。 java.lang.ClassLoader 类的方法 loadClass () 封装了代理模式的实现。

  • 该方法会首先调用 findLoadedClass () 方法来检查该类是否已经被加载过;
  • 如果没有加载过的话,会调用父类加载器的 loadClass () 方法来尝试加载该类;
  • 如果父类加载器无法加载该类的话,就调用 findClass () 方法来查找该类。

因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass () 方法,而是覆写 findClass () 方法。 下面是一个文件系统类加载器的例子:

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
public class FileSystemClassLoader extends ClassLoader { 
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}

双亲委派模型

下图展示的类加载器之间的层次关系,称为类加载器的 “双亲委派模型”。双亲委派模型要求除了顶层的启动类加载器外,其它类加载器必须有自己的父加载器。

这里的类加载器之间的父子关系一般不通过继承(Inheritance)来实现,而是通过组合(Composition)关系来服用父加载器代码。 双亲委派模型并不是一个强制性约束,而是 Java 设计者推荐给开发者的一种类加载实现方式。

双亲委派模型的工作过程

  • 如果一个类加载器收到了类加载的请求,它不会先自己尝试处理这个请求,而是委派给它的父类加载器,所有的请求最终都会传送到顶层的启动类加载器
  • 只有当父类反馈自己无法完成该请求(它的搜索范围中没有找到所需的类,即抛出 ClassNotFoundException)时,子加载器才会尝试自己加载。

为什么使用双亲委派模型?

使用双亲委派模型可以使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类 java.lang.Object,它存放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器加载,因此 Objcet 类在程序中的各种类加载器环境中都是同一个类。 如果没有使用双亲委派模型,那么如果用户自己写了一个称为 “java.lang.Object” 的类,并放在程序的 classpath 中,那么系统将产生多个不同的 Object 类,可想而知,程序将一片混乱。

双亲委派模型的实现

双亲委派模型的实现非常简单,几乎所有的代码仅在 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
//双亲委派模型的实现源码  
protected synchronized Class<?> loadClass(String name, Boolean resolve) throws ClassNotFoundException{
// 1、首先检查请求的类是否已经被加载过
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){ // 2、如果没有则调用父加载器的loadClass()方法
c = parent.loadClass(name, false);

// 3、如果父加载器为空则默认使用启动类加载器作为父加载器
} else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
// 4、如果父类加载器加载失败,则先抛出ClassNotFoundException
}
// 5、然后再调用自己的findClass()方法进行加载
if(c == null){
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}