JVM系列 —— 类的生命周期之链接和类初始化

这是 JVM 系列的第四篇,见 JVM 系列

写在前面

在前一篇的文章里面,说了说 JVM 类的生命周期第一阶段 —— 加载,加载主要干了几件事情:

  1. 通过类全限定名得到类的二进制字节流;
  2. 在内存中生成代表这个类的 java.lang.Class 对象,作为方法区中这个类的各种数据入口;
  3. 静态的存储结构转化为方法的运行时数据结构;
    接下来是链接和初始化阶段。总得来说,链接阶段就是把符号引用解析为直接引用,也就是把指向常量池的索引,换成实际的物理内存地址的阶段,而初始化阶段就是执行类构造器方法 <clinit>()的过程。

链接

链接这部分又分为三个阶段,校验准备解析。 实际上想想,在被 JVM 使用之前,还需要对 class 进行校验等操作。对于链接的三个阶段,基于类的生命周期图上再增加一些说明,如下:

链接的三个阶段

校验部分的规则还有很多,java 虚拟机规范定了很多校验规则。在链接阶段,准备的过程中,实际上那些变量已经被存放到了方法区。

类初始化

类初始化阶段总结起来就是一句话:调用 <clinit>() 的方法

那么这里的 <clinit>() 是个什么东东呢?

这个方法是编译器(javac)在编译期间自动生成的,java 类中如果有静态变量或者静态代码块,那么编译后的字节码中会包含一个 <clinit> 方法。这个方法只能有 JVM 在运行期间调用,即 java 类的初始化。

注意哦!!!java 类编译后的字节码中也会包含一个 <init> 的方法哦,这个是类的构造器方法,不要把<clinit><init> 混淆了。

举个栗子,如下图:

clinit方法

JVM 规定几种情况下,类或接口才会初始化:

  • 执行以下指令时:newgetstaticputstaticinvokestatic
  • 首次调用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void InstanceKlass::call_class_initializer_impl(instanceKlassHandle this_oop, TRAPS) {
if (ReplayCompiles &&
(ReplaySuppressInitializers == 1 ||
ReplaySuppressInitializers >= 2 && this_oop->class_loader() != NULL)) {
// Hide the existence of the initializer for the purpose of replaying the compile
return;
}

methodHandle h_method(THREAD, this_oop->class_initializer());
assert(!this_oop->is_initialized(), "we cannot initialize twice");
if (TraceClassInitialization) {
tty->print("%d Initializing ", call_class_initializer_impl_counter++);
this_oop->name()->print_value();
tty->print_cr("%s (" INTPTR_FORMAT ")", h_method() == NULL ? "(no method)" : "", (address)this_oop());
}
if (h_method() != NULL) {
JavaCallArguments args; // No arguments
JavaValue result(T_VOID);
JavaCalls::call(&result, h_method, &args, CHECK); // Static call (no args)
}
}

其中 class_initializer() 函数就是找有没有 clinit 的方法,代码如下:

class_initializer()函数
1
2
3
4
5
6
7
8
Method* InstanceKlass::class_initializer() {
Method* clinit = find_method(
vmSymbols::class_initializer_name(), vmSymbols::void_method_signature());
if (clinit != NULL && clinit->has_valid_initializer_flags()) {
return clinit;
}
return NULL;
}

如果找到了,就直接通过 JavaCalls::call() 执行初始化方法。

从上面的源码可知,如果 java 类中没有静态字段或者静态代码块 static{},编译后的字节码就不会包含 clinit 方法,上面的逻辑就不会执行。

结合上篇,类加载机制基本上就是这样的一个流程,加载 -> 链接 -> 初始化的逻辑,当然 JVM 是个多线程的程序,这些阶段必定是糅杂在一起的,不管怎样,上面执行的顺序是不会变得,但是并不是按照顺序执行的。比如加载和验证是交叉进行的,在加载开始之后,立即启动了文件格式的校验,只有在通过了校验以后,二进制字节流才会存入方法区。

JVM系列 —— 类的生命周期之链接和类初始化

https://caolizhi.top/2021-12-JVM系列-链接和类初始化/

作者

操先森

发布于

2021-12-02

更新于

2021-12-02

许可协议

评论