目录
简介
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和 初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载和 连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是 依赖运行期动态加载和动态连接这个特点实现的。例如,如果编写一个使用接口的J仆 用程序,可以等到运行时再指定其实际的实现。这种组装应用程序的方式广泛应用 Java程序之中。
为了避免语言表达中可能产生的偏差,先设立两个语口的约定:
第一,在实际情况中,每个Class文件都有可能代表着Java语言中的一个类或接口,后文中直接对“类”的描述都包括了类和接口的可能性,而对于类和接口需要 分开描述的场景会特别指明;
第二,"Class文件”并非指Class必须是存在于具体磁盘中的某个文件,这里说的Class文件指的是一串二进制的字节流,无论以何 种形式存在都可以。
类加载过程简介
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始 化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。
其中验证、准备和 解析三个部分统称为连接(Linking), 这七个阶段的发生顺序如图
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加 载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以 在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或 晚期绑定)。请注意这里写的是按部就班地“开始“,而不是按部就班地”进行”或“完 成“,因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程 中调用或激活另外一个阶段。
什么情况下需要开始类加载过程的第一个阶段:加载。虚拟机规范中并没有进行强 制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机则是严格规定了有且只有四种情况必须立即对类进行“初始化”(而加载、验证、准 备自然需要在此之前开始):
类加载的过程
加载
“加载" (Loading)阶段是“类加载" (Class Loading)过程的一个阶段,不要混淆这两个看起来很相似的名词。在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据 的访问入口。
虚拟机规范的这三点要求实际上并不具体,因此虚拟机实现与具体应用的灵活度相 业大。例如“通过一个类的全限定名来获取定义此类的二进制字节流”,并没有指明二进 制字节流要从一个Class文件中获取,准确地说是根本没有指明要从哪里获取及怎样获 取。虚拟机设计闭队在加载阶段搭建了一个相当开放的、广阔的舞台,Java发展历程中, 充满创造力的开发人员们则在这个舞台上玩出了各种花样,许多举足轻重的Java技术都 建立在这一基础之上,例如:
- 从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
- 从网络中获取,这种场景最典型的应用就是Applet。
- D运行时计算生成,这种场呆使用得最多的就是动态代理技术,在java.lang.
- reflect.Proxy中,就是用了ProxyCienerator. generateProx yClass来为特定接口生成 *$Proxy的代理类的二进制字节流。
- 由其他文件生成,典型场景: JSP应用。
- 从数据库中读取,这种场景相对少见些,有些中间件服务器(如SAP Netweaver) 可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二 进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段既可以使用系统提供 的类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员们可以通过定 义自己的类加载器去控制字节流的获取方式。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的 体数据结构。然后在Java堆中实例化一个java.lang.Class类的对象,这个对象将作为 序访问方法区中的这些类型数据的外部接口。
加载阶段与连接阶段的部分内容(如一 部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的起始时间仍然保持着固定的先后顺序。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含 的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
Java语言本身是相对安全的语言(依然是相对于C / C++来说),使用纯粹的Java代 码无法做到诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转 到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。但前面已经说过, Class文件并不一定要求用Java源码编译而来,可以使用任何途径,包括用十六进制编 辑器直接编写来产生Class文件。在字节码的语言层面上,上述Java代码无法做到的事 情都是可以实现的,至少语义上是可以表达出来的。虚拟机如果不检查输入的字节流, 对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚 拟机对自身保护的一项重要工作。
尽管验证阶段是非常重要的,并且验证阶段的工作量在虚拟机的类加载子系统中 占了很大一部分,但虚拟机规范对这个阶段的限制和指导显得非常笼统,仅仅说如果验证到输入的字节流不符合Class文件的存储格式,就抛出一个java.lang. Verify Error异常或其子类异常,具体应当检查哪些方面,如何检查,何时检查,都没 有强制要求或明确说明,所以不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟 机处理。这一阶段可能包括下面这些验证点: