这里写目录标题
7.3 类加载的过程
接下来我们会详细了解Java虚拟机中类加载的全过程,即加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。
7.3.1 加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,希望读者没有混淆 这两个看起来很相似的名词。在加载阶段,Java虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
《Java虚拟机规范》对这三点要求其实并不是特别具体,留给虚拟机实现与Java应用的灵活度都是 相当大的。例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条规则,它并没有指明二 进制字节流必须得从某个Class文件中获取,确切地说是根本没有指明要从哪里获取、如何获取。仅仅 这一点空隙,Java虚拟机的使用者们就可以在加载阶段搭构建出一个相当开放广阔的舞台,Java发展历 程中,充满创造力的开发人员则在这个舞台上玩出了各种花样,许多举足轻重的Java技术都建立在这 一基础之上,例如:
·从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
·从网络中获取,这种场景最典型的应用就是Web Applet。
·运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用 了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
·由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
·从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择 把程序安装到数据库中来完成程序代码在集群间的分发。
·可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文 件来保障程序运行逻辑不被窥探。
·……
相对于类加载过程的其他阶段,非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进 制字节流的动作)是开发人员可控性最强的阶段。加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用 程序获取运行代码的动态性。
对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接在 内存中动态构造出来的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(ElementType,指的是数组去掉所有维度的类型)最终还是要靠类加载器来完成加载,一个数组类(下面简称 为C)创建过程遵循以下规则:
·如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型,注意和前面的元素类型区分开来)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标 识在加载该组件类型的类加载器的类名称空间上(这点很重要,在7.4节会介绍,一个类型必须与类加载器一起确定唯一性)。
·如果数组的组件类型不是引用类型(例如int[]数组的组件类型为int),Java虚拟机将会把数组C 标记为与引导类加载器关联。
·数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为public,可被所有的类和接口访问到。
加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中 了,方法区中的数据存储格式完全由虚拟机实现自行定义,《Java虚拟机规范》未规定此区域的具体 数据结构。类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象, 这个对象将作为程序访问方法区中的类型数据的外部接口。
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段 尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部 分,这两个阶段的开始时间仍然保持着固定的先后顺序。
7.3.2 验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
Java语言本身是相对安全的编程语言(起码对于C/C++来说是相对安全的),使用纯粹的Java代码 无法做到诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码 行之类的事情,如果尝试这样去做了,编译器会毫不留情地抛出异常、拒绝编译。但前面也曾说过, Class文件并不一定只能由Java源码编译而来,它可以使用包括靠键盘0和1直接在二进制编辑器中敲出 Class文件在内的任何途径产生。上述Java代码无法做到的事情在字节码层面上都是可以实现的,至少 语义上是可以表达出来的。Java虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为 载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟 机保护自身的一项必要措施。
验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻 击,从代码量和耗费的执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。但是《Java虚拟机规范》的早期版本(第1、2版)对这个阶段的检验指导是相当模糊和笼统 的,规范中仅列举了一些对Class文件格式的静态和结构化的约束,要求虚拟机验证到输入的字节流如 不符合Class文件格式的约束,就应当抛出一个java.lang.VerifyError异常或其子类异常,但具体应当检查 哪些内容、如何检查、何时进行检查等,都没有足够具体的要求和明确的说明。直到2011年《Java虚 拟机规范(Java SE 7版)》出版,规范中大幅增加了验证过程的描述(篇幅从不到10页增加到130 页),这时验证阶段的约束和验证规则才变得具体起来。受篇幅所限,本书中无法逐条规则去讲解, 但从整体上看,验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
1.文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶 段可能包括下面这些验证点: