java的⽣命周期
⼀.类的⽣命周期总览
类从被加载到虚拟机内存中开始, 到卸载出内存为⽌, 它的整个⽣命周期包括: 加载
( Loading) 、 验证( Verification) 、 准备( Preparation) 、 解析(Resolution) 、 初始化( Initialization) 、 使⽤
( Using) 和卸载( Unloading) 7个阶段。 其中验证、 准备、 解析3个
部分统称为连接( Linking)
加载、 验证、 准备、 初始化和卸载这5个阶段的顺序是确定的, 类的加载过程
必须按照这种顺序按部就班地开始, ⽽解析阶段则不⼀定: 它在某些情况下可以在初始化阶段之后再开始, 这是为了⽀持Java语⾔的运⾏时绑定( 也称为动态绑定或晚期绑定) 。这些阶段通常都是互相交叉地混合式进⾏的, 通常会在⼀个阶段执⾏的过程中调⽤、 激活
另外⼀个阶段。
什么时候开始类加载呢?
1.遇到new、 getstatic、 putstatic或invokestatic这4条字节码指令时, 如果类没有进⾏过初始化, 则需要先触发其初始化。 ⽣成这4条指令的最常见的Java代码场景是: 使⽤new关键字实例化对象的时候、 读取或设置⼀个类的静态字段( 被final修饰、 已在编译期把结果放⼊常量池的静态字段除外) 的时候, 以及调⽤⼀个类的静态⽅法的时候。
2.使⽤flect包的⽅法对类进⾏反射调⽤的时候, 如果类没有进⾏过初始化,则需要先触发其初始化。
3.当初始化⼀个类的时候, 如果发现其⽗类还没有进⾏过初始化, 则需要先触发其⽗
类的初始化。
4.当虚拟机启动时, ⽤户需要指定⼀个要执⾏的主类( 包含main( ) ⽅法的那个
类) , 虚拟机会先初始化这个主类。
5.当使⽤JDK 1.7的动态语⾔⽀持时, 如果⼀个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、
REF_putStatic、 REF_invokeStatic的⽅法句柄, 并且这个⽅法句柄所对应的类没有进⾏过初始化, 则需要先触发其初始化。
注意:
当⼀个类在初始化时, 要求其⽗类全部都已经初始化过了, 但是⼀个接⼝在初始化时, 并不要求其⽗接⼝全部都完成了初始化, 只有在真正使⽤到⽗接⼝的时候( 如引⽤接⼝中定义的常量) 才会初始化。
⼆:类的加载过程
a.加载
“加载”是“类加载”( Class Loading) 过程的⼀个阶段,在加载阶段, 虚拟机需要完成以下3件事情:
1.通过⼀个类的全限定名,来获取定义此类的⼆进制字节流
2.将这个字节流所代表的静态存储结构转化为⽅法去的运⾏时数据结构
3.在内存中⽣成⼀个代表这个类的java.lang.Class对象,作为⽅法去这个类的各种数据的访问⼊⼝。
加载阶段完成后, 虚拟机外部的⼆进制字节流就按照虚拟机所需的格式存储在⽅法区之
中, ⽅法区中的数据存储格式由虚拟机实现⾃⾏定义, 虚拟机规范未规定此区域的具体数据结构。 然后在内存中实例化⼀个
java.lang.Class类的对象( 并没有明确规定是在Java堆中, 对于HotSpot虚拟机⽽⾔, Class对象⽐较特殊, 它虽然是对象, 但是存放在⽅法区⾥⾯) , 这个对象将作为程序访问⽅法区中的这些类型数据的外部接⼝。
b.验证
static修饰的变量验证是连接阶段的第⼀步, 这⼀阶段的⽬的是为了确保Class⽂件的字节流中包含的信息符合当前虚拟机的要求, 并且不会危害虚拟机⾃⾝的安全。
验证阶段⼤致上会完成下⾯4个阶段的检验动作: ⽂件格式验证、元数据验证、 字节码验证、 符号引⽤验证。
1.⽂件格式验证
第⼀阶段要验证字节流是否符合Class⽂件格式的规范,并且能被当前版本的虚拟机处 理。该验证阶段的主要⽬的是保证输⼊的字节流能正确地解析并存储于⽅法区 之内,格式上符合描述⼀个Java类型信息的要求。这阶段的验证是基于⼆进制字节流进⾏ 的,只有通过了这个阶段的验证后,字节流才会进⼊内存的⽅法区中进⾏存储,所以后⾯的 3个验证阶段全部是基于⽅法区的存储结构进⾏的,不会再直接操作字节流。
2.元数据验证
第⼆阶段是对字节码描述的信息进⾏语义分析,以保证其描述的信息符合Java语⾔规范 的要求,这个阶段可能包括的验证点如下: 这个类是否有⽗类(除了java.lang.Object之外,所有的类都应当有⽗类)。 这个类的⽗类是否继承了不允许被继承的类(被final修饰的类)。如果这个类不是抽象类,是否实现了其⽗类或接⼝之中要求实现的所有⽅法。等.
第⼆阶段的主要⽬的是对类的元数据信息进⾏语义校验,保证不存在不符合Java语⾔规 范的元数据信息。
3.字节码验证
第三阶段是整个验证过程中最复杂的⼀个阶段,主要⽬的是通过数据流和控制流分析, 确定程序语义是合法的、符合逻辑的。在第⼆阶段对元数据信息中的数据类型做完校验后, 这个阶段将对类的⽅法体进⾏校验分析,保证被校验类的⽅法在运⾏时不会做出危害虚拟机 安全的事件.
4.符号引⽤验证
最后⼀个阶段的校验发⽣在虚拟机将符号引⽤转化为直接引⽤的时候,这个转化动作将 在连接的第三阶段——解析阶段中发⽣。符号引⽤验证可以看做是对类⾃⾝以外(常量池中 的各种符号引⽤)的信息进⾏匹配性校验.
对于虚拟机的类加载机制来说,验证阶段是⼀个⾮常重要的、但不是⼀定必要(因为对 程序运⾏期没有影响)的阶段。如果所运⾏的全部代码(包括⾃⼰编写的及第三⽅包中的代 码)都已经被反复使⽤和验证过,那么在实施阶段就可以考虑使⽤-Xverify:none参数来关 闭⼤部分的类验证措施,以缩短虚拟机类加载的时间。
c.准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使⽤的内存 都将在⽅法区中进⾏分配。这个阶段中有两个容易产⽣混淆的概念需要强调⼀下,⾸先,这 时候进⾏内存分配的仅
包括类变量(被static修饰的变量),⽽不包括实例变量,实例变量将 会在对象实例化时随着对象⼀起分配在Java堆中。其次,这⾥所说的初始值“通常情况”下是 数据类型的零值
在“通常情况”下初始值是零值,那相对的会有⼀些“特殊情况”:如果类字段 的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为 ConstantValue属性所指定的值
d.解析
解析阶段是虚拟机将常量池内的符号引⽤替换为直接引⽤的过程/
符号引⽤(Symbolic References):符号引⽤以⼀组符号来描述所引⽤的⽬标,符号可 以是任何形式的字⾯量,只要使⽤时能⽆歧义地定位到⽬标即可。符号引⽤与虚拟机实现的 内存布局⽆关,引⽤的⽬标并不⼀定已经加载到内存中。各种虚拟机实现的内存布局可以各 不相同,但是它们能接受的符
号引⽤必须都是⼀致的,因为符号引⽤的字⾯量形式明确定义 在Java虚拟机规范的Class⽂件格式中。
直接引⽤(Direct References):直接引⽤可以是直接指向⽬标的指针、相对偏移量或是 ⼀个能间接定位到⽬标的句柄。直接引⽤是和虚拟机实现的内存布局相关的,同⼀个符号引 ⽤在不同虚拟机实例上翻译出来的直接引⽤⼀般不会相同。如果有了直接引⽤,那引⽤的⽬ 标必定已经在内存中存在。
解析动作主要针对类或接⼝、字段、类⽅法、接⼝⽅法、⽅法类型、⽅法句柄和调⽤点 限定符7类符号引⽤进⾏,分别对应于常量池的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info、 CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7种常量类型
1.类或接⼝的解析 假设当前代码所处的类为D,如果要把⼀个从未解析过的符号引⽤N解析为⼀个类或接 ⼝C的直接引⽤,那虚拟机完成整个解析的过程需要以下3个步骤: 1)如果C不是⼀个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去 加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,⼜可能触发其他相关 类的加载动作,例如加载这个类的⽗类或实现的接⼝。⼀旦这个加载过程出现了任何异常, 解析过程就宣告失败。
2)如果C是⼀个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类 似“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符 如前⾯所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机⽣成⼀个代表此数组维度和元素的数组对象。
3)如果上⾯的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为⼀个有效的类 或接⼝了,但在解析完成之前还要进⾏符号引⽤验证,确认D是否具备对C的访问权限。如 果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。
2.字段解析 要解析⼀个未被解析过的字段符号引⽤,⾸先将会对字段表内class_index 项中索引的 CONSTANT_Class_info符号引⽤进⾏解析,也就是字段所属的类或接⼝的符号引⽤。如果在 解析这个类或接⼝符号引⽤的过程中出现了任何异常,都会导致字段符号引⽤解析的失败。 如果解析成功完成,那将这个字段所属的类或接⼝⽤C表⽰,虚拟机规范要求按照如下步骤 对C进⾏后续字段的搜索。
1)如果C本⾝就包含了简单名称和字段描述符都与⽬标相匹配的字段,则返回这个字段 的直接引⽤,查结束。
2)否则,如果在C中实现了接⼝,将会按照继承关系从下往上递归搜索各个接⼝和它的 ⽗接⼝,如果
接⼝中包含了简单名称和字段描述符都与⽬标相匹配的字段,则返回这个字段 的直接引⽤,查结束。
3)否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其⽗ 类,如果在⽗类中包含了简单名称和字段描述符都与⽬标相匹配的字段,则返回这个字段的 直接引⽤,查结束。
4)否则,查失败,抛出java.lang.NoSuchFieldError异常。 如果查过程成功返回了引⽤,将会对这个字段进⾏权限验证,如果发现不具备对字段 的访问权限,将抛出java.lang.IllegalAccessError异常。
3.类⽅法解析
类⽅法解析的第⼀个步骤与字段解析⼀样,也需要先解析出类⽅法表的class_index 项中 索引的⽅法所属的类或接⼝的符号引⽤,如果解析成功,我们依然⽤C表⽰这个类,接下来 虚拟机将会按照如下步骤进⾏后续的类⽅法搜索。
1)类⽅法和接⼝⽅法符号引⽤的常量类型定义是分开的,如果在类⽅法表中发现 class_index中索引的C是个接⼝,那就直接抛出
java.lang.IncompatibleClassChangeError异常。
2)如果通过了第1步,在类C中查是否有简单名称和描述符都与⽬标相匹配的⽅法, 如果有则返回这个⽅法的直接引⽤,查结束。
3)否则,在类C的⽗类中递归查是否有简单名称和描述符都与⽬标相匹配的⽅法,如 果有则返回这个⽅法的直接引⽤,查结束。
4)否则,在类C实现的接⼝列表及它们的⽗接⼝之中递归查是否有简单名称和描述符 都与⽬标相匹配的⽅法,如果存在匹配的⽅法,说明类C是⼀个抽象类,这时查结束,抛 出java.lang.AbstractMethodError异常。
5)否则,宣告⽅法查失败,抛出java.lang.NoSuchMethodError。 最后,如果查过程成功返回了直接引⽤,将会对这个⽅法进⾏权限验证,如果发现不 具备对此⽅法的访问权限,将抛出java.lang.IllegalAccessError异常。
4.接⼝⽅法解析
接⼝⽅法也需要先解析出接⼝⽅法表的class_index [4]项中索引的⽅法所属的类或接⼝的符 号引⽤,如果解析成功,依然⽤C表⽰这个接⼝,接下来虚拟机将会按照如下步骤进⾏后续 的接⼝⽅法搜索。
1)与类⽅法解析不同,如果在接⼝⽅法表中发现class_index中的索引C是个类⽽不是接 ⼝,那就直
接抛出
java.lang.IncompatibleClassChangeError异常。
2)否则,在接⼝C中查是否有简单名称和描述符都与⽬标相匹配的⽅法,如果有则返 回这个⽅法的直接引⽤,查结束。
3)否则,在接⼝C的⽗接⼝中递归查,直到java.lang.Object类(查范围会包括 Object类)为⽌,看是否有简单名称和描述符都与⽬标相匹配的⽅法,如果有则返回这个⽅ 法的直接引⽤,查结束。
4)否则,宣告⽅法查失败,抛出java.lang.NoSuchMethodError异常。 由于接⼝中的所有⽅法默认都是public的,所以不存在访问权限的问题,因此接⼝⽅法 的符号解析应当不会抛出java.lang.IllegalAccessError异常。
e.初始化
类初始化阶段是类加载过程的最后⼀步,前⾯的类加载过程中,除了在加载阶段⽤户应 ⽤程序可以通过⾃定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化 阶段,才真正开始执⾏类中定义的Java程序代码.
在准备阶段,变量已经赋过⼀次系统要求的初始值,⽽在初始化阶段,则根据程序员通 过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外⼀个⾓度来表达:初始 化阶段是执⾏类构造器<clinit>()⽅法的过程。
<clinit>()⽅法是由编译器⾃动收集类中的所有类变量的赋值动作和静态语句块 (static{}块)中的语句合并产⽣的,编译器收集的顺序是由语句在源⽂件中出现的顺序所决 定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前 ⾯的静态语句块可以赋值,但是不能访问.
<clinit>()⽅法与类的构造函数(或者说实例构造器<init>()⽅法)不同,它不 需要显式地调⽤⽗类构造器,虚拟机会保证在⼦类的<clinit>()⽅法执⾏之前,⽗类的< clinit>()⽅法已经执⾏完毕。因此在虚拟机中第⼀个被执⾏的<clinit>()⽅法的类肯定 是java.lang.Object。
由于⽗类的<clinit>()⽅法先执⾏,也就意味着⽗类中定义的静态语句块要优先于⼦ 类的变量赋值操作.
<clinit>()⽅法对于类或接⼝来说并不是必需的,如果⼀个类中没有静态语句块,也 没有对变量的赋值操作,那么编译器可以不为这个类⽣成<clinit>()⽅法。
虚拟机会保证⼀个类的<clinit>()⽅法在多线程环境中被正确地加锁、同步,如果多 个线程同时去初始化⼀个类,那么只会有⼀个线程去执⾏这个类的<clinit>()⽅法,其他 线程都需要阻塞等待,直到活动线程执⾏<clinit>()⽅法完毕。如果在⼀个类的<clinit>()⽅法中有耗时很长的操作,就可能造成多个进程阻塞
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论