说明
1 类加载(Class Loading)包括加载,验证,准备,解析,和初始化五个部分.
2 这个文档里介绍的是加载,验证和准备
加载
加载(Loading)是类加载(Class Loading)过程中的一个阶段,在加载时,虚拟机完成了三件事情:
(1) 通过类的全额定名来获取定义此类的二进制字节流.
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3) 在Java堆里生产一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口.
可以看出来,其实虚拟机规范中这三点要求都不具体.它甚至没有规定类的二进制字节流必须是通过javac编译产生的.正因为虚拟机规范在加载阶段的开发,才造成后面好多举足轻重的Java技术都建立在这个基础上.比如
(1) 从ZIP包获取二进制流,最终产生了jar,ear,war等格式.
(2) 网络中获取,比如Applet应用.
(3) 运行时计算生成,比如java.lang.reflect.Proxy类运行时创建对象.
(4) ….
加载阶段完成以后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区
中,方法区中的数据存储格式由具体的虚拟机自行定义.然后在Java堆中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些数据的外部接口.
验证
验证阶段的目的,是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全.
Java语言本身是相对安全的语言,如果通过正常的途径编译java类,是基本不会出现不符合虚拟机要求的字节码文件的.但是在之前”加载”这一节说过,java的二进制字节码不一定是通过javac命令编译过来,它可以使用很多途径.甚至你可以通过十六进制编辑器(比如WinHex)直接编写Class文件(当然也可以修改).所以,如果不做验证,危害还是非常大的.
虽然虚拟机规范没有对验证做明确的规定,交给具体的虚拟机来完成.但是每个虚拟机大致上都会完成下面四个阶段:
文件格式验证
这个阶段验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理.这个阶段大概包括:
(1) 魔数是否为CAFEBABE
(2) 主次版本号是否在当前虚拟机处理范围内
(3) 常量池的常量是否有不被支持的常量类型
(4) 指向常量池的索引值是否有指向不存在的常量或者不符合类型的常量
(5) Class文件中各个部分及文件本身是否有被删除的或者附加的其他内容.
(6) ….
上面举了一些验证的规则.但是,注意这个不是验证的全部.这个阶段的验证是基于字节流进行的,经过了这个阶段的验证以后,字节流才会进入内测的方法区进行存储.后面的三个验证阶段全部都是基于方法区的存储结构进行的.
元数据验证
这个阶段的验证是字节码的描述信息进行语义分析,保证其描述的信息符合Java语言规范的要求.这个阶段的验证包括:
(1) 这个类是否有父类.
(2) 这个类的父类是否继承了不允许被继承的类(被final修饰的类).
(3) 如果这个类不是抽象类,是否实现了其父类或者接口的所有抽象方法
(4) 类中的字段,方法是否与父类产生了矛盾.
(5) ….
字节码验证
这个阶段是整个验证过程中最复杂的.主要工作是进行数据流和控制流分析.其实这个阶段就是对类的方法体进行校验分析.例如
(1) 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,比如不会在操作栈中放置一个int类型的数据,使用时却按照Long类型来加载
(2) 保证跳转的指令不会跳转到方法体以外的字节码上
(3) 保证方法体中类型的转换是有效的.
(4) ….
这个验证十分的复杂,而且有一条原则,如果一个类的方法如果字节码没有验证通过,
那说明肯定有问题,但是验证码通过的方法不一定是没有问题的..
正因为这个验证太花时间,所以jdk提供了一些方法来关闭这个验证.比如之前在eclipse优化那里说到的 -Xverify:none 指令就是关闭字节码验证的.
符号引用校验
这个校验阶段发现在虚拟机将符号引用转换为直接引用的时候.这个转换动作是在解析阶段中发生的.通常的校验内容包括:
(1) 符号引用中通过字符串描述的全额定名是否能找到对应的类
(2) 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段.
(3) 符号引用中的类,方法和字段的隔离级别是否可被当前类访问.
(4) ….
符号引用的校验确保解析动作能正常执行.如果校验不通过,则会抛出java.lang.IncompatibleClassChangeError异常的子类.比如我们常见的java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSushMethodError.
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段.这些内容都要在方法区中进行分配.这里需要注意两地啊你
(1)注意这里说的是类变量(也就是说被static修饰的),而不包括实例变量.实例变
量将在对象实例化时随着对象一起分配到Java堆中.
(2)另外一点需要注意的是,这里所说的初始值通常情况下是该类型的零值.这句话本身又需要解释两点,第一,零值是什么.比如
public static int value = 123;
在准备阶段,value值就会被初始化为0..因为Int对应的零值为0,而float类型对应的零值为0f.long类型为0f, 引用类型为null.其实这些都容易理解.
第二,这里说的是,通常情况下是会被赋值为零值,换句话说,还有特殊情况下是会被直接赋值的.这种特殊情况就是:如果该类字段的字段属性表中存放ConstantValue属性,那么准备阶段就会将value的值进行初始化.比如
public static final int value = 123;
这个时候在准备阶段就会 把value值初始化为123;
在最后,我还是解释一下属性表中的ConstantValue属性.由于属性表太过复杂,所以我在写本篇文档的时候并没有将类结构解析中属性表相关部分的文档写完.既然这边说到了属性表中的ConstantValue属性,所以还是解释一下. ConstantValue属性的作用是通过虚拟机自动为静态变量赋值.只有被static修饰的变量才可以使用这项属性.但是这个只是前提,并不是说所有被static修饰的变量都会有对应的ConstantValue属性.在sun的hotspot虚拟机是这么定义的:只有同时使用final 和static修饰的变量(其实就是常量了),并且这个变量的类型是基本数据类型或者String类型,才会在属性表中生成ConstantValue属性.