从Java虚拟机的角度来看,类加载器只有两种:
- 启动类加载器,由C++实现,作为虚拟机本身的一部分;
- 其他类加载器,由Java实现,独立于虚拟机,且全部继承自抽象类java.lang.ClassLoader.
从开发人员的角度来看,类加载器可以再细分一点:
-
启动类加载器:
负责加载JAVA_HOME/lib下的类库,或者通过-Xbootclasspath指定的路径,且只能够加载被虚拟机识别的文件名,如果随便放入一个test.jar于这个目录下,也不会被启动类加载器加载,无法被Java程序直接引用,如图:
-
扩展类加载器:
负责加载JAVA_HOME/lib/ext目录中类库,或者被java.ext.dirs系统变量指定的路径下的类库,开发者可以直接使用扩展类加载器。
-
应用程序类加载器:
负责加载用户路径上所指定的类库,默认就是系统的类加载器。
-
自定义类加载器:
必要情况下, 也可以自定义类加载器。
双亲委派模型
类加载器之间的关系如图所示:
这种层次关系就是双亲委派模型,除了顶层的启动类加载器,其他的类加载都有自己的父亲类加载器,它的工作过程如下:
- 当自定义类加载器收到了一个加载请求,不会自己直接去加载这个请求,而是将请求交给应用程序类加载器;
- 应用程序类加载器同样不会直接加载,而是将请求交给扩展类加载器;
- 扩展类加载器将请求交给启动类加载器,如果启动类加载器能够找到所需要的类,那么就自己去处理这个请求,如果无法找到,那么就将请求回传给下一级,同理,下一级类加载的处理也同理。
为什么使用双亲委派模型
好处是显而易见的,例如,当我们要加载java.lang.Object这个类时,由于它是放在JAVA_HOME\lib\rt.jar之中,那么它的加载过程,从上面的过程可以知道,无论是哪一个类加载器来加载它,最终会交给启动类加载器来加载,这样就能够保证Object类不管在哪个加载器环境下都是指的同一个类,避免了如果用户自己编写了一个java.lang.Object,并放在自己的classPath下,导致一个程序中出现多个Object类,使得Java类型体系出现了混乱的行为。如果实现一个自定义加载器,强行用defineClass()去加载一个"java.lang"开头的类也不会成功,会收到虚拟机抛出的一个SecurityException异常。
双亲委派模型的实现
实现很简单,从ClassLoader的loadClass()就可以看到双亲委派的实现过程:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 1. 首先检查该类是否已经加载过了,如果加载过了,直接返回,否则就进入下面的双亲委派过程
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2. 如果父类加载不为空,则请求父类加载器去加载
c = parent.loadClass(name, false);
} else {
//3.如果父类加载器为空,说明是启动类加载器,即是Bootstrap加载器,那么请求它去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类加载无法完成加载请求,则抛出ClassNotFoundException异常。
}
//4.如果父类加载器无法加载时,调用自身类加载器去加载
if (c == null) {
c = findClass(name);
}
}
return c;
}