在上一篇文章《JVM学习笔记(三):Java内存模型》中,我总结了Java内存模型相关的一些知识。接下来,我将继续,参考着周志明老师的《深入理解Java虚拟机》,以及一些自己查阅的书籍、资料,总结一下JVM类加载机制相关的知识。
一个类型从 被加载到虚拟机内存 开始,到 卸载出内存未知,它的整个生命周期将会经历 加载、验证、准备、解析、初始化、使用 和 卸载 七个阶段,其中 验证、准备 和 解析 三个阶段统称为 连接。如下图:
上图中,加载、验证、准备 和 初始化 五个阶段的顺序是一定的:类的加载过程必须按照这个顺序依次 开始 ,而解析阶段却不一定。某些情况下,解析 可能在 初始化 之后再进行。
上面这句话,对于标红的 开始 的理解:开始的意思是,五个阶段依次开始,但并不一定按照这个顺序 进行或完成。这几个阶段通常都是交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
上面简单介绍了讲了类加载涉及到的几个过程,下面就分别介绍一下。
在加载阶段,Java虚拟机需要完成三件事情:
验证 的主要目的是:确保 Class 文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
1、文件格式验证。
这一阶段主要验证字节流是否符合Class文件格式的规范,并且要确保能被当前版本的虚拟机处理。只有在验证通过之后,字节流才被允许进入Java虚拟机内存的方法区中进行存储。
2、元数据验证
这一阶段的主要目的是对类的元数据信息进行语义检验。比如,这个类是否有父类(java.lang.Object 除外)等。
3、字节码验证
在第二阶段 元数据验证完成后,就要进行字节码验证,以保证检验类的方法在运行时不会做出危害虚拟机安全的行为。
4、符号引用验证
符号引用验证,主要就是校验 该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。符号引用验证的主要目的是确保 解析 行为能正常进行。
准备阶段是 正式为类中定义的静态变量(static)分配内存并设置 初始值 的阶段。
注意,这里说的设置的初始值,一般情况下是数据类型的“零值”。比如:
private static int value = 123;
上面这条语句,在准备阶段完成后,value 的值是 0 ,而不是123。将 value 赋值为 123 的操作在 初始化阶段 才会被执行。
而对于 final 修饰的静态变量,则会在准备阶段就被设置为对应的值。比如:
private static final int value = 123;
上面这条语句,在 javac 编译时,将会为 value 属性生成 ConstantValue 属性,在准备阶段虚拟机就回根据 ConstantValue 将 value 赋值为 123。
解析阶段是 Java虚拟机将常量池内的符号引用替换为直接引用的过程。
这里,我也没看太懂,回头补充吧。
在准备阶段,变量已经被赋过一次系统要求的“零值”,而在初始化阶段会根据我们所写的代码去初始化 变量 和 其他资源。
《Java虚拟机规范》中规定了有且仅有下面六种情况,如果类还没初始化,则必须对类先进行初始化:
上面是 有且仅有,也就是说:
实现“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作的代码被称为“类加载器”。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。也就是说,判断两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两类就必定不“相等”。这里的相等,指的是 euqals()、instanceof、isInstance() 等情况。
从虚拟机的角度看,只存在两种不同的类加载器:
从开发人员角度,Java一直保持着三层类加载器、双亲委派模型架构。
绝大多数的Java程序都会使用到以下三个系统提供的类加载器来进行加载:
1、启动类加载器(Bootstrap ClassLoader)
这个类加载器用于加载
启动类加载器 无法被Java程序直接引用。如果我们在自定义类加载器时,需要把请求委派给引导类加载器去处理,那直接 返回 null 代替即可。
2、扩展类加载器(Extension ClassLoader)
扩展类加载器 负责加载
扩展类加载器 所有代码都是Java实现的,开发人员可以直接使用它来加载Class文件。
3、应用程序类加载器(Application ClassLoader)
由于 应用程序类加载器 是 ClassLoader类中getSystemClassLoader()方法的返回值,有时候它也被称为“系统类加载器”。它负责加载用户类路径(ClassPath)上的所有类库。开发者同样也可以直接使用应用程序类加载器,一般情况下它也是程序中默认的类加载器。
JDK9之前的Java应用都是由这三种类加载器互相配合来完成加载的,用户还可以加入自定义的类加载器来进行拓展。这些类加载器之间的协作关系通常如下图所示:
上图中,各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”。
双亲委派模型要求 除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。不过,类加载器之间的父子关系不是通过继承来实现的,而是通过 组合 来复用父加载器的代码。
双亲委派模型的工作流程是:如果一个类加载器收到了加载类的请求,他不会尝试首先自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该被传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到指定的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型来组织加载器之间的关系,一个显而易见的好处就是:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object 类,它存放于 rt.jar 中,无论哪一个类加载器要加载这个类,最终都会被委派到 启动类加载器中进行加载,因此 Object 类在程序的各种加载器环境中都能保证是同一个类。
抄了一遍周老师的书,感觉又有很多新的收获~
1、《深入理解Java虚拟机》第三版,周志明 著,第七章。