从上图可知,加载(loading)这一步包含了三个更加细粒度的模块,分别为 BootStrap Class Loader,Extention Class Loader 和 Application Class Loader,这三个 Class Loader 就是我们加载过程中必须要使用到的三大类加载器。
动态加载:无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包jar、war中,网络中,本地文件等。
全盘负责:当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。
定义:启动(Bootstrap)类加载器也称为引导类加载器,该加载器是用本地代码实现的类加载器,它所加载的类库绝大多数都是出自 %JAVA_HOME%/lib 下面的核心类库,当然还有其他少部分所需类库。
1.4 扩展(Extension)类加载器定义:扩展类加载器负责将 %JAVA_HOME%/lib/ext 或者少数由系统变量 -Djava.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
1.5 系统(System Application)类加载器 定义:系统类加载器负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中。开发者可以直接使用系统类加载器。
Tips:系统(System Application)类加载器加载的核心类库类型比较多,也会加载 lib 下的未被 BootStrap 类加载器加载的类库,还会加载 ext 文件夹下的未被 Extension 类加载器加载的类库,以及其他类库。总之一句话,加载除了 BootStrap 类加载器和 Extension 类加载器所加载的其余的所有的核心类库。
向上委托:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。如果父类加载器可以完成类加载任务,就成功返回;
向下委派:倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
下面以加载 /jre/lib/plugin.jar为例,它是需要被系统(System Application)类加载器加载的核心类库,下图是它的加载流程图。
从上图可知,对于核心类库 plugin.jar 的加载,分为以下 6 步:
步骤 1:plugin.jar 会先通过自定义类加载器(前提是我们实现了自定义类加载器),自定义类加载器不会做处理,直接向上委托给系统(System Application)类加载器;
步骤 2:系统(System Application)类加载器接到委托后,也不做任何处理,直接向上委托给扩展(Extension)类加载器;
步骤 3:扩展(Extension)类加载器接到委托后,也不做任何处理,直接向上委托给启动(Bootstrap)类加载器;
步骤 4:启动(Bootstrap)类加载器接到委托后,发现 plugin.jar 不是自己负责加载的核心类库,于是进行向下委派,委派给扩展(Extension)类加载器;
步骤 5:扩展(Extension)类加载器接到委派后,发现 plugin.jar 也不是自己负责加载的核心类库,于是进行向下委派,委派给系统(System Application)类加载器;
步骤 6:系统(System Application)类加载器接到委派后,发现 plugin.jar 是自己负责加载的核心类库,于是进行加载,最后成功加载了 plugin.jar。
除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。
public abstract class ClassLoader {
//每个类加载器都有个父加载器
private final ClassLoader parent;
public Class> loadClass(String name) {
//查找一下这个类是不是已经加载过了
Class> c = findLoadedClass(name);
//如果没有加载过
if( c == null ){
//先委派给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}
return c;
}
protected Class> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class> defineClass(byte[] b, int off, int len){
...
}
}
2.4 为什么要双亲委派
双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。
双亲委派的主要目的就是保证java官方的类库
例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。
如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。
破坏双亲委派有两种方式:
第一种,自定义类加载器,需要继承ClassLoader、重写findClass和loadClass;
第二种,通过线程上下文类加载器的传递性,让父类加载器调用子类加载器的加载动作。
注意:双亲委派的破坏只能发生在AppClassLoader及其以下的加载委派顺序,ExtClassLoader上面的双亲委派是不能破坏的!
从上图可知,链接(linking)包含了三个更加细致的步骤,分别为验证(verify),准备(prepare)和解析(resolve)。
3.1.1 验证 定义:验证是链接阶段的第一步,其目的是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全。
主要验证信息:验证过程中,主要对三种类型的数据进行验证,分别是“元数据验证,字节码验证和符号引用验证”。
a.元数据验证
验证这个类是否有父类(除了 java.lang.Object 之外,所有类都应当有父类);
验证这个类是否继承了不允许被继承的类(被 final 修饰的类);
如果这个类不是抽象类,验证该类是否实现了其父类或接口之中所要求实现的所有方法;
验证类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。
b.字节码验证
字节码验证的主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中;
保证跳转指令不会跳转到方法体以外的字节码指令上;
保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。
c.符号引用验证
符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:
符号引用中通过字符串描述的全限定名是否能够找到对应的类;
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
符号引用中的类、字段、方法的访问性(private、default、protected、public)是否可被当前类访问。
定义:准备阶段是正式为类变量分配内存并设置类变量默认值(通常情况下是数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
变量默认值对照表如下:
| 变量类型 | 默认值 |
|---|---|
| Int | 0 |
| Long | 0L |
| Short | 0 |
| Char | ‘u0000’ |
| Byte | 0 |
| Boolean | false |
| Folat | 0.0f |
| Double | 0.0d |
| Reference | null |
定义:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。
具体的解析内容:解析过程中,主要对如下4种类型的数据进行验证:
类或接口的解析;
字段解析;
类方法解析;
接口方法解析。
定义:在链接的准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员制定的主观计划去初始化类变量和其他资源。
类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
实例的初始化顺序如下图所示:
实例的初始化顺序是非常重要的知识点,在面试过程中也经常涉及到这个知识点,上图的加载顺序需要重点掌握。
ps:以上内容来自对慕课教程的学习与总结。



