原本是想写一篇关于Java类加载机制的博文,后来发现这个主题有点大,其中涉及的细节点太多,一篇博文,三言两语恐怕无法讲明白,于是乎决定从整体到局部,先来谈谈类的生命周期,从整体把握一个类从“出生”到“凋亡”的过程,其中涉及了类加载、使用、卸载等各个阶段,有了整体的认知后,再深入细节并结合具体实例,探讨加载原理、类加载器等相关知识。今天就让博主带领你开启第一段旅程:类的生命周期详解。
类的生命周期是指一个class从加载到内存直至卸载出内存的过程,共包含 加载 (Loading)、 验证 (Verification)、 准备 (Preparation)、 解析 (Resolution)、 初始化 (Initialization)、 使用 (Using)和 卸载 (Unloading)7个阶段,如下图所示:
其中验证、准备、解析三个阶段统称为 连接 (Linking),而加载、连接、初始化又可以统称为 类加载的过程 ,所以我们有时又可以称类的生命周期包含加载、连接、初始化、使用和卸载这5个阶段,或者是类加载、使用、卸载这3个阶段。
回到上图,加载、验证、准备、初始化和卸载这5个阶段的 开始顺序 是确定的,如图中箭头所示。之所以强调“开始顺序”,是因为这里的先后顺序仅仅是各阶段开始时间的顺序,而不是进行或完成的顺序,这些阶段 通常是相互交叉地混合式进行的 。比如加载和验证,并不是说非要等到加载完成之后,才开始验证阶段,在加载的阶段中,会穿插各种检验动作,否则对于连格式都不符合的字节流,又怎能正确解析出其中的静态数据结构从而转化为方法区中的数据结构呢?对于解析阶段, 其开始时间则比较特殊 ,既可能在加载阶段就开始(对常量池中的符号引用的解析),也可能在初始化阶段之后才开始(支持Java语言的动态绑定)。
下面我们就来看看各个阶段都大致做哪些事情。
类加载的过程包含 加载 、 连接 和 初始化 三个阶段。
加载是类加载过程的第一阶段,此时虚拟机将查找并加载类的二进制数据,具体分为三个步骤:
这三条属于虚拟机规范的内容,只指明了做什么,具体实现交由虚拟机实现自行安排,这就给了虚拟机实现和具体应用足够的灵活度。对于第一条,并未指明定义类的二进制字节流的存储形式(class文件、ZIP包)、来源(本地文件系统、内存或网络)以及获取方式(既可以从已有静态资源读取也可动态生成),因而就有了如下的多样可能性:
对于第三条中所说的“内存”,虚拟机规范并没有明确规定是在Java堆还是方法区中,对于我们最为熟悉的 HotSpot 虚拟机,是存放在Java堆的永久代中。实际上永久代是 HotSpot 虚拟机特有的,是它对虚拟机规范中方法区概念的具体实现( JDK1.7及以下 ),对于其他虚拟机(如 IBM J9 )是不存在永久代一说的,关于方法区和永久代的关系超出本博文的谈论范畴了,点到为止。
加载阶段完成后,原本定义类的二进制字节流就按照虚拟机所需的格式存储在方法区中,这里的存储格式依具体的虚拟机实现而定,各有差异,虚拟机规范并未规定此区域的具体数据结构。
org.sherlockyb.test.HelloWorld
,定义一维数组类 HelloWorld[] hws = new HelloWorld[8]
,虚拟机会直接创建名为“[Lorg.sherlockyb.test.HelloWorld”的数组类,并对其进行初始化。 上一节中加载阶段的第一步骤——“通过一个类的全限定名来获取定义此类的二进制字节流”,就是类加载器所做的唯一工作,类加载器是Java技术体系中的重要基石,它在类层次划分、OSGi、热部署、代码加密等领域扮演着重要角色,关于它我们暂且不做细致介绍,后面会有单独博文深入探讨之。
虚拟机规范并未强制规定加载阶段具体什么时候开始,由虚拟机实现自由把握。就我们所熟知的 HotSpot 虚拟机来说,有两种情况:
连接可细分为三个阶段:验证、准备和解析。
连接的第一个阶段,确保从class文件中所加载的字节流符合当前虚拟机的要求,且 不会危害虚拟机自身的安全 。该阶段会依次进行如下校验:
从上面可以看出,验证阶段非常重要,关乎虚拟机的安全, 但它并不是必须的 ,它对程序运行期没有影响,如果所引用的类已被反复使用和验证过,那么可以考虑采用 -Xverifynone
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。通常来讲,应用所加载的class文件都是由我们本地或服务器的JDK编译通过的,我们都确定它是符合虚拟机要求的,对于这类class文件其实并不需要验证,主要是像从网络加载的class字节流或是通过动态字节码技术生成的字节流,出于安全的考虑,是必须要经过严格验证的。
准备阶段做的唯一一件事就是为类的静态变量分配内存,并将其初始化为默认值。注意这里的初始化和后面要讲的“初始化阶段”是不同的,容易混淆。 这些内存都在方法区中分配 。几点注意项:
public static final int len = 5
,在准备阶段 len
的值已经被设置为5了。实际上对于final的类变量,在编译时就已经将其结果放入了调用它的类的常量池中,这种类变量的访问并不会触发其所属类的初始化阶段。 该阶段把类在常量池中的符号引用转为直接引用。符号引用就是一组用来描述目标的字面量,说白了就是静态的占位符,与内存布局无关,而直接引用则是运行时的,是指内存中直接指向目标的指针、相对偏移量或间接定位到目标的句柄。解析工作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符这7类符号引用,将其替换为直接引用。
虚拟机规范规定,在执行 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield 和 putstatic 这16个用于操作符号引用的字节码指令之前,必须先对符号引用进行解析。至于具体时间并未要求,交由虚拟机实现自行决定:在类被加载时就对常量池中的符号引用进行解析(静态指令,除invokedynamic之外的),或是等到一个符号引用将要被使用前才去解析(动态指令:invokedynamic,为了支持动态绑定)。
为类的静态变量赋予程序设定的初始值。在Java中对类变量设定初始值有两种方式:声明类变量时指定初始值和静态代码块为静态变量赋值。我们来看下类的初始化步骤:
我们可以从字节码层面获知上述初始化步骤的原理,
编译器在编译Java源文件时,自动收集类中所有类变量的赋值操作和静态语句块中的语句( 按照源码中声明先后顺序 ),将其合并产生 /
虚拟机规范严格规定,当发生对一个类的 主动引用 时,会立即触发类的初始化阶段。 主动引用 有且仅有以下5种情况:
除此之外,其他所有引用类的方式都属于 被动引用 ,不会触发初始化。
包括主动引用和被动引用,前者在上节已有说明,我们来列举几个被动引用的实例:
A[] arr = new A[8]
,并不会触发A的初始化。 当一个类被判定为无用类时,才可以被卸载。条件苛刻,需要同时满足如下条件:
对于满足上述3个条件的无用类,虚拟机可以对其回收,但并不是必然的,是否回收可通过 -Xnoclassgc
参数控制。 注意 :在大量使用反射、动态代理等字节码框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代(特指HotSpot虚拟机)不会溢出。
终于算是“走马观花”般地把Java类的生命周期过了一遍,相信当再提起类的生命周期时,大家脑海里就会立马浮现出类生命周期的大纲,都有哪些阶段,每个阶段都大致做些什么事情,都有些什么注意点,这样,本博文的目的就达到了!掌握了全局之后,接下来就是细节的探讨,比如像验证阶段中的字节码验证,实际是非常复杂的,虚拟机专门为此做了诸多优化;再比如解析阶段,7类符号引用各自不同的解析细节又是什么,等等之类。之后,笔者将会单独另起博文,针对类加载器、解析阶段等进行详细分析,敬请期待。