什么是类加载?
我们编写的代码,通过编译器的编译,变成class文件,也就是字节码。在服务启动的过程中JVM就会读取这些class文件,然后通过一系列操作,按照JVM的规范存放到内存中指定的位置,这个过程就是类加载。如下图展示的是类的整个生命周期:
Tip
加载 → 连接 → 初始化都是属于类加载的范围,使用和卸载不属于类加载的步骤。
什么是类加载器
既然类加载是JVM的活,为什么又要搞个类加载器暴露出来呢?在《深入理解Java虚拟机》中是这样描述的:
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。
而实现上述动作的代码就被称为是“类加载器”(Class Loader)。
Important
类加载器的主要作用是把类文件加载到内存,但是他的影响不止于此,类加载器会影响到类的唯一性,同一个类文件如果被两个类加载器加载,那么他们就不是同一个类了。
Java原生的类加载器
先来了解一下Java原生的类加载器。
类加载器分两种,一种是启动类加载器(Bootstrap ClassLoader),这个加载器是native code实现的,是虚拟机的一部分;另一种是其他类加载器。
启动类加载器(Bootstrap ClassLoader)负责加载<JAVA_HOME>\lib目录下的代码,也就是Java自己的代码。
但是实际上除了启动类加载器,扩展类加载器和应用程序加载器也是Java自带的,所以从开发者的角度看,Java自带的类加载器就是3个,是一个三层的结构。
- 扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。根据“扩展类加载器”这个名称,就可以推断出这是一种Java系统类库的扩展机制,JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能,在JDK 9之后,这种扩展机制被模块化带来的天然的扩展能力所取代。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件;
- 应用程序类加载器(Application Class Loader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
除了Java原生的类加载器,我们还可以自定义自己的类加载器来是实现我们的需求。
[!NOTE]
启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。
什么是双亲委派模型?
Important
首先双亲委派模型不是一个具有强制约束力的模型,只是一个最佳实践,毕竟Java本身也出现了多处违反这个模型的地方。
Important
双亲委派模型这个翻译并不好,双亲在中文语境下基本是指父母两人,所以很容易误导,既然parent class翻译为父类,那么Parent Delegation Model是不是也应该翻译为父类委派模型呢?
双亲委派模型的思路是这样的:
| 首先启动类加载器作为顶层的类加载器,其他的类加载器每一个都有一个自己的父类,这样所有的类加载器会组成一个树的结构,启动类加载器就是根节点。 |
类加载的工作流程定义如下:
- 当一个类接收到类加载的请求时,应该首先把请求委派给父类,看看父类是否能加载这个类;
- 当父类收到这个类加载请求时,仍然需要首先委派给自己的父类,直到请求传递到启动类加载器;
- 启动类加载器判断自己是否能加载这个类,如果启动类加载器可以加载那么就会自行加载这个类;
- 如果启动类加载器无法加载,那么会反馈给调用自己的子类,子类会尝试自己加载,如果自己也无法加载,那么会同样反馈给调用自己的子类;
- 以此类推,直到有一个加载器能加载这个类为止;
Note
这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码,也就是说,子类并不是用super调用。
使用双亲委派模型的好处:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,类加载的请求一定会传递到顶层的启动类加载器,从上至下依次判断自己是否能够加载,这样就有了一种确定性,例如rt.jar中的类一定会是启动类加载器来加载,而不会是一个用户自定义的类加载器加载的。