学习参考资料:周志明老师的著作《深入理解Java虚拟机(第3版)》
我们知道Java代码经编译后会生成Class文件,然后都需要加载到虚拟机中才能被运行和使用。
而虚拟机如何加载这些Class文件,Class文件中的信息进入到虚拟机会发生什么变化,接下来就会围绕这两个问题展开。
1.类的生命周期
一个类型从被加载到虚拟机中开始,到卸出内存,它的生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,如下图。
图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班的开始,而解析则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也成为动态绑定)。
注意:这里的加载只是类加载过程的一个阶段
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,也因此为Java应用提供了极高的可扩展性和灵活性,Java天生可以动态扩展的语言特征就是依赖运行期动态加载和动态连接这个特点实现的
2.类加载的过程
类加载的全过程即加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。
加载
在加载阶段,Java虚拟机主要完成下面三件事情:
1)通过类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表此类的java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
对于数组类而言,数组类本身不需要类加载器创建,它是由Java虚拟机直接在内存中创建。但是数组类中的元素还是需要类加载器来完成加载。
验证
验证是连接阶段的第一步,这一阶段的主要目的是确保Class文件的字节流符合《Java虚拟机规范》的全部约束要求,保证这些代码运行后不会危害虚拟机的自身安全。大致会完成下面四个阶段的验证动作:
-
文件格式验证 第一阶段要验证字节流是否符合Class文件格式的规范, 并且能被当前版本的虚拟机处理。 主要目的是保证输入的字节流能正确地解析并存储于方法区之内, 格式上符合描述一个Java类型信息的 要求。 这阶段的验证是基于二进制字节流进行的, 只有通过了这个阶段的验证后, 字节流才会进入内存的方法区中 进行存储, 所以后面的3个验证阶段全部是基于方法区的存储结构进行的, 不会再直接操作字节流。 -
元数据验证 第二阶段是对字节码描述的信息进行语义分析, 以保证其描述的信息符合Java语言规范的要求。 主要目的是对类的元数据信息进行语义校验, 保证不存在不符合Java语言规范的元数据信息。 -
字节码验证 第三个阶段将对类的方法体进行校验分析, 保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。 主要目的是通过数据流和控制流分析, 确定程序语义是合法的、 符合逻辑的。 -
符号引用验证 第四个阶段可以看做是对类自身以外( 常量池中的各种符号引用) 的信息进行匹配性校验。 这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候, 这个转化动作将在连接的第三阶段——解析阶段中发生。
对于虚拟机的类加载机制来说, 验证阶段是一个非常重要的、 但不是一定必要( 因为对程序运行期没有影响)的阶段。 如果所运行的全部代码( 包括自己编写的及第三方包中的代码) 都已经被反复使用和验证过, 那么在实施阶段就可以考虑使用参数来关闭大部分的类验证措施, 以缩短虚拟机类加载的时间。
准备
准备阶段是为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设初始值的阶段(静态变量分配在方法区)。
需要注意的是:这里仅仅指的是静态变量,而实例变量分配内存要在实例化的时候进行了。这里的初始值并不是将值直接赋值,而是数据类型的”零值“,如下:
public static int value=123;
value的初始值不会被赋成123 ,而是0 ;赋值成123 要在初始化阶段才能完成。
但假如将代码改成,如下:
public static final int value=123;
这样子就会在准备阶段,为静态常量value 赋值成123了。
解析
解析阶段是将常量池中的符号引用改为直接引用的过程,解析动作主要针对类或接口、 字段、 类方法、 接口方法、 方法类型、 方法句柄和调用点限定符7类符号引用进行。
- 符号引用( Symbolic References) : 符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面
量, 只要使用时能无歧义地定位到目标即可。 - 直接引用( Direct References) : 直接引用可以是直接指向目标的指针、 相对偏移量或是一个能间接定位到
目标的句柄。
初始化
前面几个阶段中,都是有虚拟机主导和控制(除了在加载阶段用户程序可以通过自定义类加载器)。到了初始化这个阶段,就真正开始执行类中定义的Java程序。
在准备阶段, 类变量已经赋过一次系统要求的初始值, 而在初始化阶段, 则根据程序员通过程序制定的主观计划去初始化类变量和其他资源, 或者可以从另外一个角度来表达: 初始化阶段是执行类构造器<clinit>() 方法的过程。根据准备阶段的例子:
public static int value=123;
在这个阶段才会被赋值为123;
3.类加载器
3.1类与类加载器
一个类本身和加载这个类的加载器共同确立才能在Java虚拟机中具有唯一性,也就是说,即使来自同一份Class文件,被不同的加载器加载,两个类也是不”相等“的(这里的相等指的是instanceof和Class类的equals的返回值)。
绝大多数Java程序都会使用到以下3个系统提供的类加载器来加载:
1)启动类加载器(Bootstrap ClassLoader)
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
2)扩展类加载器(Extension ClassLoader)
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
3)应用程序类加载器(App ClassLoader)
负责加载classpath中指定的jar包及目录中class
另外,JVM的类加载机制主要有如下3种:
- 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
- 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
3.2双亲委派模型
如图,各类加载器之间的层次关系就被称为加载器的双亲委派模型。
双亲委派模型的工作过程:如果一个类加载器收到了加载类的请求,它首先会委托其上层类加载器加载,上层接到请求再交给它的上层,依次递归下去,直到启动类加载器。如果本层类加载器不能加载才会让其子层进行加载。
使用双亲委派模型的好处:无论哪一个类加载器加载这个类,最终都会委派给最顶端的启动类加载器,因此Object类在各种加载环境下都能保证是同一个类。假如没有采用这种双亲委派模型进行加载,而是由各自的类加载器进行加载,那么如果自定义了一个java.lang.Object ,那么系统中就可能会有多个Object,这就会导致Java类型体系中最基础的行为都无法保证。
后面还会陆陆续续更新这系列的读书笔记,期待您的关注~~
|