前言:
在这一节,主要描述的就是虚拟机是如何读取Class文件的?
首先,虚拟机会把描述类的数据从Class文件中加载到内存,并对数据进行校验,转换解析、连接和初始化,最终形成可以直接使用的Java类型,这就是虚拟机的类加载机制
类加载时机
类从被夹在到虚拟机内存中开始,整个生命周期为:
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)其中验证,准备解析,又统称为连接
加载、验证、准备、初始化和卸载,这五个阶段的顺序是确定的,但是解析阶段就不一定,它在某个阶段可能会在初始化之后再开始,这是为了支持java语言的动态绑定。
对于加载时机并不确定,但是对于初始化的时间是规定了的,在遇到以下五种情况的时候,必须立即对类进行“初始化”。并且有且只有这五种场景中的欣慰称之为对一个类进行主动引用,除此之外,所有的引用类的方式都不会出发初始化
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化那么需要先出发起初始化。这四条指令的使用场景分别为:使用new关键字实例化对象、赌气或者设置一个类的静态字段以及调用一个类的静态方法
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 初始化一个类,发现父类还没有初始化的时候必须先初始化其父类
- 当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
- 当使用动态语言时候,如果一个java.lang.invoke.MethodHandle的实例最后的解析结果REF_getStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有初始化
这里有几个需要注意的地方,我当初是很容易搞不清楚
/**
* @author Eric
* @date create in 2019/4/27
*
* 被动使用类型
* 通过子类引用弗雷的静态字段,不会导致子类初始化
*/
public class SupperClass {
static {
System.out.println("this is supper class init");
}
public static int value = 111;
}
public class SubClas extends SupperClass {
static {
System.out.println("sub class init");
}
}
/**
* @author Eric
* @date create in 2019/4/27
*/
public class App {
public static void main(String[] args) {
System.out.println(SubClas.value);
}
}
上面这段代码运行完之后,只会输出“his is supper class init
” 这是因为对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会出发父类的初始化而不会出发子类的初始化
假如我们把父类的value换成静态常量那么当我们在调用的时候也不会输出“his is supper class init
”。这是因为在Java源码中引用了SuperClass类中的常量Value,但是其实在编译阶段通过常量传播优化
,已经将此常量值111存储到App类的常量池
中,以后App这个类对常量SuperClass.value的引用时机都被转化为App类对自身常量吃的引用了,
类加载过程
加载
加载的过程需要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,
作为方法区这个类的各种数据访问入口
对于非数组类型的加载阶段,是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义类加载器去完成这个工作(重写类加载器的loadClass()方法)
对于数组类而言,数组类本身不通过类加载器来创建,它是由java虚拟机直接创建的,但是数组类的元素类型(Element Type 数组去掉所有维度的类型)最终还是要考类加载器来创建,数组类的创建过程遵循以下规则
- 如果数组的组件类型(Component Type 指的是数组去掉一个维度的类型)是引用类型,那就递归的采用加载过程去加载这个组件类型
- 如果数组的组件类型不是引用类型,Java虚拟机将会把数组类标记为与引导类加载器关联起来
- 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性默认为public
加载阶段完成之后,虚拟机外部的而精致字节流就按照虚拟机需要的格式存储在方法区中。
验证
验证是连接阶段的第一步,这一阶段的目的就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。验证阶段非常重要,这一阶段直接决定了java虚拟机是否能够承受恶意的代码攻击。而且验证阶段的工作量在虚拟机的类加载子系统中又占用了相当大的一部分,如果验证要的字节流不符合规范,那么虚拟机就会抛出一个java.lang.VerifyError异常或者其子类异常
验证的内容有很多,以下列出几点:
- 是否以魔数0xCAFEBABE开头
- 主次版本号是否在当前虚拟机处理范围之内
- 常量池的唱两种是否有不被支持的常量类型
- 只想常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info类型的常量中是否有不符合UTF8编码的数据
只有通过了验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证阶段完全是基于方法区的存储结构进行的
- 元数据的验证
- 这个类是否有父类
- 这个类的父类是否继承了不允许被继承的类
- 如果这个类不是抽象类,是否实现了其父类或借口中要实现的所有方法
- 类中的字段,方法是否与父类产生矛盾
- 字节码验证
- 保证任何试课操作数栈的数据类型与指令代码序列都能配合工作(
例如不会出现这种情况,再造错栈中放置了一个int类型的数据,使用时却按照long类型加载入本地变量表
) - 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法中的类型转换是有效的,(例如把子类对象赋值给父类数据类型是安全的,但是把父类对象赋值给子类数据类型是不安全的)
- 保证任何试课操作数栈的数据类型与指令代码序列都能配合工作(
- 符号引用的验证,(
这个验证动作将在连接的第三阶段--解析阶段中发生,符号引用验证可以看做是对类自身以外的信息进行匹配性校验
)
- 符号引用中,通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在复核方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用的类,字段,方法的访问权限是否可以被当前类访问
扶摇引用验证的目的就是确保解析动作是否能够正常执行
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中进行分配,值得注意的是,这个时候进行内存分配的仅仅包括类变量,而不包括实例变量
,实例变量会在对象实例化时随着对象一起分配在java堆中,其次这里所说的初始值指的数据类型的零值而不是实际使用的值
但是如果某字段的字段属性表中存在ConstanValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的值
例如:一个类的类变量value定义为
public static final int value=123
public static int value =1234
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,在Class文件中,它以CONSTANT_CLASS_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info,等类型的常量出现,那解析阶段所说的直接引用与符号引用又有什么关系呢?
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何心事的字面量,只要使用的时候能够无歧义的定位到目标即可
- 直接引用(Direct References):可以是直接指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。直接引用适合虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实力上翻译出来的直接引用一般不会相同
解析的动作主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符七类符号引用进行分别对应常量池中的CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
、CONSTANT_InterfaceMethodref_info
、CONSTANT_MethodType_info
、CONSTANT_MethodHandle_info
、CONSTANT_InvokeDynimic_inof
、
- 类或者接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主管计划去初始化类变量和其它资源。也就是说初始化阶段是执行类构造器()方法的过程,
< clinit>()
方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,< clinit>()
方法与类的构造函数(实例构造器< init>()
)不同,它不需要现实的调用父类构造器,虚拟机会保证在子类< clinit>()
方法执行之前,父类的< clinit>()
)方法已经执行完毕。因此在虚拟机中第一个被执行的< clinit>()
肯定是java.lang.Object- 由于父类的
< clinit>()
方法先执行,也就意味着弗雷中定义的静态语句块要优先于子类的变量赋值操作
-< clinit>()
方法对于类或者接口来说并不是必须的,如果一个类中没有静态语句块,也就没有对变量的复制操作,那么编译器也就不会生成< clinit>()
方法了 - 接口中不会使用静态语句块,但是任然有变量初始化的复制操作,因此接口与类一样都会生成
< clinit>()
方法,但是接口鱼类不同的是,执行接口的< clinit>()
方法不需要先执行父接口的< clinit>()
方法,只要当父接口定义的变量使用时候父接口才会初始化另外接口的实现类在初始化时候也一样不会执行接口的< clinit>()
方法 - 虚拟机会保证一个类的
< clinit>()
方法在多线程环境中被正确的枷锁、同步
以上类容为本人读书笔记,摘录书中本章节比较重要的知识点而已