类的生命周期
类的生命周期分为以下7个阶段:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载,其中验证、准备、解析阶段又统称为连接。
一、加载
加载阶段有以下三个步骤:
- 通过类全限定名获取这个类的二进制字节流
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
PS:在HotSpot中,第三阶段生成的Class对象是保存在方法区的。
二、验证
验证是连接阶段的第一步,这个阶段的目的是检查字节流中包含的信息是否符合虚拟机的要求,确保不会危害虚拟机自身的安全。它包括如下四个方面的验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
1.文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能够被虚拟机处理,要验证的方面有:
是否以魔数0xCAFEBABE开头
主、次版本号是否在当前虚拟机处理范围内
常量池中的常量是否有不被支持的常量类型
指向常量的索引值是否有不存在的常量或者不符合类型的常量
CONSTANT_Utf8_info型的常量是否符合UTF-8编码格式
......
除了以上这些,还有很多需要验证的地方,上面只是一小部分。通过了这个阶段的验证,字节流中的静态数据结构会保存在方法区中,后面三个阶段的验证都是基于方法区的存储结构进行的。
2.元数据验证
第二阶段对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,要验证的方面有:
这个类是否具有父类
这个类的父类是否继承了不被继承的类
如果这个类不是抽象类,是否实现了父类或接口中要求实现的所有方法
类中的字段、方法是否矛盾(例如覆盖了父类的final字段,或者方法重载不符合规则)
......
3.字节码验证
第三阶段将对类的方法体进行校验分析,保证方法是合法的、符合逻辑的,不会做出危害虚拟机的行为,要验证的方面有:
保证任意时刻操作数栈的数据类型和指令序列都能配合工作,例如不会出现这样的情况:对int型数据操作却用了对float型数据进行操作的指令
保证跳转指令不会跳转到方法体以外的字节码上
保证类型转换是正确的,例如可以把一个子类对象赋值给父类变量,但反过来不可以
......
4.符号引用验证
第四阶段可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,要验证的方面有:
根据类的全限定名字符串能否找到相应的类
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
符号引用中的类、字段、方法能否被当前类访问
......
三、准备
准备阶段是为类变量(静态变量)分配内存空间并设置变量初始值,这里的初始值是虚拟机内部设置的,并不是用户给变量赋予的值,这里也可以说是设置零值,例如int型变量的零值是0,float、double型变量的零值是0.0,更多类型的零值如下表:
假如变量是final的,并且是基本数据类型或者String类型,那在准备阶段就会设置为被显式赋予的值。
四、解析
解析是把符号引用替换为直接引用的过程。
符号引用:通俗的讲,符号引用可以说是一些字符串,比如com.example.ClassA中使用了com.example.ClassB,那么ClassA的字节码会出现com/example/ClassB这样的字符串(符号)来说明引用了com.example.ClassB。
直接引用:直接引用可以是
- 指向目标的指针(比如:指向类型[Class对象]、指向类变量、指向类方法的直接引用可能是指向方法区的指针)
- 相对偏移量(比如指向实例变量、实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
1.类或接口的解析
-
解析的类不是数组
假如当前类是A,要解析类B,虚拟机会把类B的全限定名传递给加载类A的类加载器去加载它。 -
解析的类是数组
假如当前类是A,要解析类B的数组,会像上面那样加载类B,最后生成一个代表此数组维度和元素的对象。 -
确保有访问权限
最后进行符号引用验证,确认有解析的类的访问权限。
2.字段解析
对字段的解析,要先解析该字段所在的类或接口(假设该类为C),再按下面步骤搜索字段:
① 如果C包含了简单名称和描述符都符合的字段,则返回这个字段的直接引用。
② 否则,查找C实现的接口(递归搜索该接口的父接口),如果有简单名称和描述符都符合的字段,则返回这个字段的直接引用。
③ 否则,如果C不是java.lang.Object,则查找C的父类,如果有简单名称和描述符都符合的字段,则返回这个字段的直接引用。
④ 否则,查找失败,抛出java.lang.NoSuchFieldException异常。
查找到匹配的字段时,要检查是否有该字段的访问权限,如果没有,则抛出java.lang.IllegalAccessError异常。
3.类方法解析
对类方法的解析,也是要先解析该方法所在的类(假设该类为C),再按下面步骤搜索方法:
① 首先检查C是不是一个类,如果发现类C是个接口,会抛出java.lang.IncompatibleClassChangeError异常。
② 如果C中有简单名称和描述符都符合的方法,则返回此方法的直接引用。
③ 否则,如果C不是java.lang.Object,则递归查找C的父类,如果有简单名称和描述符都符合的方法,则返回这个方法的直接引用。
④否则,查找C实现的接口(递归搜索该接口的父接口),如果有简单名称和描述符都符合的方法,说明该方法是抽象方法,C是抽象类,抛出java.lang.AbstractMethodError异常。
⑤ 否则,查找失败,抛出java.lang.NoSuchMethodError异常。
查找到匹配的方法时,要检查是否有该方法的访问权限,如果没有,则抛出java.lang.IllegalAccessError异常。
4.接口方法解析
对接口方法的解析,也是要先解析该方法所在的接口(假设接口为I),再按下面步骤搜索方法:
① 先检查I是不是一个接口,如果I是一个类,会抛出java.lang.IncompatibleClassChangeError异常。
② 否则,如果I中有简单名称和描述符都符合的方法,则返回此方法的直接引用。
③ 否则,递归查找I的父接口和java.lang.Object,如果有简单名称和描述符都符合的方法,则返回此方法的直接引用。
④ 否则,宣告查找失败,抛出java.lang.NoSuchMethodError异常。
五、初始化
初始化的过程就是执行类构造器<clinit>的过程(如:一些静态变量的赋值语句、静态代码块)。
一些要注意的点:
- 并非所有的类或接口都有<clinit>方法,如果类没有静态变量赋值和静态代码块、接口没有静态变量赋值,那么编译器可以不为它们生成<clinit>方法。
- 执行类的<clinit>方法不会执行它实现接口的<clinit>方法(除非用到了接口的静态变量),执行接口的<clinit>方法不会执行父接口的<clinit>方法(除非用到了父接口的静态变量)。
参考资料:
《深入理解JAVA虚拟机》-周志明