JVM系列 —— 类的生命周期之链接和类初始化
这是 JVM 系列的第四篇,见 JVM 系列。
写在前面
在前一篇的文章里面,说了说 JVM 类的生命周期第一阶段 —— 加载,加载主要干了几件事情:
- 通过类全限定名得到类的二进制字节流;
- 在内存中生成代表这个类的 java.lang.Class 对象,作为方法区中这个类的各种数据入口;
- 静态的存储结构转化为方法的运行时数据结构;
接下来是链接和初始化阶段。总得来说,链接阶段就是把符号引用解析为直接引用,也就是把指向常量池的索引,换成实际的物理内存地址的阶段,而初始化阶段就是执行类构造器方法<clinit>()
的过程。
链接
链接这部分又分为三个阶段,校验,准备,解析。 实际上想想,在被 JVM 使用之前,还需要对 class 进行校验等操作。对于链接的三个阶段,基于类的生命周期图上再增加一些说明,如下:
校验部分的规则还有很多,java 虚拟机规范定了很多校验规则。在链接阶段,准备的过程中,实际上那些变量已经被存放到了方法区。
类初始化
类初始化阶段总结起来就是一句话:调用 <clinit>()
的方法。
那么这里的 <clinit>()
是个什么东东呢?
这个方法是编译器(javac
)在编译期间自动生成的,java 类中如果有静态变量或者静态代码块,那么编译后的字节码中会包含一个 <clinit>
方法。这个方法只能有 JVM 在运行期间调用,即 java 类的初始化。
注意哦!!!java 类编译后的字节码中也会包含一个
<init>
的方法哦,这个是类的构造器方法,不要把<clinit>
和<init>
混淆了。
举个栗子,如下图:
JVM 规定几种情况下,类或接口才会初始化:
- 执行以下指令时:
new
、getstatic
、putstatic
或invokestatic
; - 首次调用
java.lang.invoke.MethodHandle
实例时,解析的种类是2(REF_getStatic
)、4(REF_putStatic
)、6(REF_invokeStatic
)、8(REF_newInvokeSpecial
)的方法句柄; - 在调用类库中的某些反射方法,比如
Class
类或java.lang.Reflect
包中的方法; - 在对某个子类进行初始化时;
- 在它被选定为java虚拟机启动时的初始类时;
从源码的角度上,在使用 new
关键字第一次实例化一个 java 类的时候,调用的接口是 hotspot/src/share/vm/oops/instanceKlass.cpp
中的 call_class_initializer_impl()
方法。源码如下:
1 | void InstanceKlass::call_class_initializer_impl(instanceKlassHandle this_oop, TRAPS) { |
其中 class_initializer()
函数就是找有没有 clinit
的方法,代码如下:
1 | Method* InstanceKlass::class_initializer() { |
如果找到了,就直接通过 JavaCalls::call()
执行初始化方法。
从上面的源码可知,如果 java 类中没有静态字段或者静态代码块 static{}
,编译后的字节码就不会包含 clinit
方法,上面的逻辑就不会执行。
结合上篇,类加载机制基本上就是这样的一个流程,加载 -> 链接 -> 初始化的逻辑,当然 JVM 是个多线程的程序,这些阶段必定是糅杂在一起的,不管怎样,上面执行的顺序是不会变得,但是并不是按照顺序执行的。比如加载和验证是交叉进行的,在加载开始之后,立即启动了文件格式的校验,只有在通过了校验以后,二进制字节流才会存入方法区。
JVM系列 —— 类的生命周期之链接和类初始化