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 | public class FileSystemClassLoader extends ClassLoader { |
双亲委派模型
下图展示的类加载器之间的层次关系,称为类加载器的 “双亲委派模型”。双亲委派模型要求除了顶层的启动类加载器外,其它类加载器必须有自己的父加载器。
这里的类加载器之间的父子关系一般不通过继承(Inheritance)来实现,而是通过组合(Composition)关系来服用父加载器代码。 双亲委派模型并不是一个强制性约束,而是 Java 设计者推荐给开发者的一种类加载实现方式。
双亲委派模型的工作过程
- 如果一个类加载器收到了类加载的请求,它不会先自己尝试处理这个请求,而是委派给它的父类加载器,所有的请求最终都会传送到顶层的启动类加载器
- 只有当父类反馈自己无法完成该请求(它的搜索范围中没有找到所需的类,即抛出 ClassNotFoundException)时,子加载器才会尝试自己加载。
为什么使用双亲委派模型?
使用双亲委派模型可以使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类 java.lang.Object
,它存放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器加载,因此 Objcet 类在程序中的各种类加载器环境中都是同一个类。 如果没有使用双亲委派模型,那么如果用户自己写了一个称为 “java.lang.Object”
的类,并放在程序的 classpath 中,那么系统将产生多个不同的 Object 类,可想而知,程序将一片混乱。
双亲委派模型的实现
双亲委派模型的实现非常简单,几乎所有的代码仅在 loadClass()
方法中实现,下面是一个简单的例子:
1 | //双亲委派模型的实现源码 |