第七章 虚拟机类加载机制
7.1 类加载特性
与那些在编译时需要进行连接工作的语言不同,在JAVA中,类型的加载和连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为JAVA应用程序提高高度的灵活性,JAVA中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
7.2 类加载过程
加载(Loading)->验证(Verification)->准备(Preparation)->解析(Resolution)->初始化(Initialization)->使用(Using)->卸载(Unloading)
** 其中验证,准备,解析这三个过程称为连接 **
** 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的 **
7.3 加载(Loading)过程
加载过程以及完成之后要做的两件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流
where&how获取二进制流?
- 从JAR、EAR、WAR格式的包中(Tomcat就是通过部署在上边的WAR包来运行整个web项目的)
- 从网络中获取(Applet应用)
- 动态代理技术(java.lang.reflect.Proxy)
- JSP文件
- 从数据库中读取(中间件服务器SAP Netweaver)
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构:即java.lang.Class对象
7.4 验证(Verification)过程
7.4.1 目的:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。(WHY?因为Class文件不仅仅可以由java源文件编译形成,也可以人为构造Class文件。如果输入的字节流不符合Class文件的存储格式,抛错:java.lang.VerifyError)
7.4.2 四大验证步骤:
- 文件格式验证:(来源于HotSpot虚拟机标准)
1.1 是否以魔数0xCAFEBABE开头
1.2 主、次版本号是否在当前虚拟机处理范围之内
1.3 常量池的常量中是否有不被支持的常量类型
1.4 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
1.5 CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
1.6 Class文件中各个部分以及文件本身是否有被删除的或附加的其他信息
1.7 more...... - 元数据验证
作用:语义验证,保证不存在不符合java语言规范的元数据信息
通常验证以下几项内容:
2.1 这个类是否有父亲(除了java.lang.Obejct之外,所有的类都应当有父类)
2.2 这个类的父亲是否继承了不允许被继承的类(被final修饰的类)
2.3 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
2.4 类中的字段、方法是否与父类产生了矛盾
2.5 more...... - 字节码验证
作用:对类的方法体进行校验分析,确保被校验类的方法在运行时不会做出危害虚拟机安全的行为
3.1 保证任意时刻操作数栈的数据类型与指令代码都能配合工作,例如不会出现类似这样的情况:在操作栈中放置了一个int类型的数据,使用的时候却按long类型来加载到本地变量表中
3.2 保证跳转指令不会跳转到方法体以外的字节码指令上
3.3 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,反之则是危险的。
3.4 more......
** 注意:如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。(Halting Problem停机问题。) ** - 符号引用验证
作用:将符号引用转化为直接引用,在"解析阶段"中发生。如无法通过验证,则抛错:IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
4.1 符号引用中通过字符串描述的全限定名是否能找到对应的类
4.2 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
4.3 符号引用中的类、字段和方法的访问行(private,protected,public,default)是否可被当前类访问
4.4 more......
7.5 准备(Preparation)过程
1.作用:正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
2.为类变量分配是指:被static修饰的变量,不包括实例变量。实例变量将会在对象实例化时随着对象一起分配在JAVA堆中。
3.这里的初始化是指"通常情况"下数据类型的零值
例子:public static int value = 123;
这里的value变量,在准备阶段过后的初始值为0,而不是123,因为这时候尚未开始执行任何JAVA方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以value = 123的动作将在初始化阶段才会被执行
4.除了上边提到的数据初始化的"通常情况",还有"特殊情况":
public static final int value = 123;
这个时候类字段的字段属性就有了ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
7.6 解析(Resolution)过程
作用:虚拟机将常量池内的符号引用替换为直接引用的过程。
** 那么,什么是符号引用和直接引用呢? **
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
- 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
** 那么,什么时候解析呢?**
虚拟机规范并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast、getfield、getstatic、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield、putstatic这13个用于操作符号引用的字节码指令之前。
** 针对的对象是什么呢? **
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型
7.7 初始化(Initialization)过程
该过程是类加载过程的最后一步。在该过程中,才开始真正执行类中定义的Java代码。(注意该过程并不是对象的实例化
)
虚拟机会执行类构造器<clinit>()方法
来初始化该类。简单归纳一下就是:
- 类的初始化:(最后一步)执行面向类的构造器(<clinit>())
- 对象的初始化:执行面向对象的构造器(Constructor)
回归正题:
类构造器<clinit>()方法
是指:该方法是由编译器自动收集类中的所有类变量
的赋值动作
和静态语句块中
的语句
合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。
注意:静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值
,但是不能访问
更多详细的细节见《深入理解Java虚拟机》7.3.5章节