本文主要关注类加载过程中的类初始化阶段,介绍clinit方法、主动引用被动引用

clinit

类(接口)初始化阶段是执行**clinit**方法的过程

接口 clinit

  1. 接口的clinit方法主要用于在接口初始化时,**初始化接口的域**(默认是public static final
  2. 编译后,接口中的,有些会在Class文件常量池中,有些在clinit方法中进行初始化

代码

1
2
3
4
5
6
7
public interface InterfaceClinit {
class InnerClass {
}

InnerClass INNER_CLASS = new InnerClass();
String NAME = "zhongmingmao";
}

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static final me.zhongmingmao.class_initialization.InterfaceClinit$InnerClass INNER_CLASS;
descriptor: Lme/zhongmingmao/class_initialization/InterfaceClinit$InnerClass;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

public static final java.lang.String NAME;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String zhongmingmao

# <clinit>() 方法
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #1 // class me/zhongmingmao/class_initialization/InterfaceClinit$InnerClass
3: dup
4: invokespecial #2 // Method me/zhongmingmao/class_initialization/InterfaceClinit$InnerClass."<init>":()V
7: putstatic #3 // Field INNER_CLASS:Lme/zhongmingmao/class_initialization/InterfaceClinit$InnerClass;
10: return

分析

  1. NAME拥有**ConstantValue属性,存储在Class文件常量池**中(Constant pool),不需要在clinit方法中进行初始化
  2. INNER_CLASS需要在clinit方法中进行初始化,对应的字节码是实现了INNER_CLASS = new InnerClass(),关于那newdupinvokespecial等指令的具体含义,可参照博文「字节码 - 方法重载 + 方法重写」,这里不再赘述

类 clinit

  1. 类clinit接口clinit最大的区别是允许**static{}**块
  2. 类clinit由类中的所有**static变量的赋值动作static{}块中的语句**合并产生
  3. static{}块只能读取定义在static{}块之前的static变量,但能为定义在static{}块之后的static变量赋值

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClassClinit {
static class InnerClass {
}

static {
j = 2;
// System.out.println(i); // 非法前向引用
}

static int i = 1;
static int j;
static InnerClass INNER_CLASS = new InnerClass();
}

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int i;
descriptor: I
flags: ACC_STATIC

static int j;
descriptor: I
flags: ACC_STATIC

static me.zhongmingmao.class_initialization.ClassClinit$InnerClass INNER_CLASS;
descriptor: Lme/zhongmingmao/class_initialization/ClassClinit$InnerClass;
flags: ACC_STATIC

# <clinit>() 方法
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: iconst_2
1: putstatic #2 // Field j:I
4: iconst_1
5: putstatic #3 // Field i:I
8: new #4 // class me/zhongmingmao/class_initialization/ClassClinit$InnerClass
11: dup
12: invokespecial #5 // Method me/zhongmingmao/class_initialization/ClassClinit$InnerClass."<init>":()V
15: putstatic #6 // Field INNER_CLASS:Lme/zhongmingmao/class_initialization/ClassClinit$InnerClass;
18: return

空 clinit

clinit方法对于类或接口来说**并不是必需**的,如果一个类中没有static{}块,也没有对static变量的赋值操作,那么编译器可以不为这个类生成clinit方法

代码

1
2
3
4
5
6
7
public class EmptyClassClinit {
// 字节码中不会有<clinit>()方法
}

interface EmptyInterfaceClinit {
// 字节码中不会有<clinit>()方法
}

隐含地并发

  1. JVM会保证一个clinit方法在多线程环境中被**正确地加锁、同步** ➜ 隐蔽地阻塞
  2. 其他线程被阻塞,但如果执行clinit方法的线程退出clinit方法后,其他线程唤醒之后**不会再次进入**clinit方法

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ClinitConcurrencyTest {
static class A {
static {
System.out.println("Class A Initialization");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
reader.readLine();
} catch (IOException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {

Runnable initClassTask = () -> {
System.out.println(String.format("%s start", Thread.currentThread().getName()));
new A(); // 隐蔽地同步,不会重复进入
System.out.println(String.format("%s run over", Thread.currentThread().getName()));
};

new Thread(initClassTask, "t1").start();
new Thread(initClassTask, "t2").start();
}
}

运行结果

1
2
3
4
5
6
t2 start
t1 start
Class A Initialization
xxxxx # 控制台输入随意字符
t2 run over
t1 run over

类初始化 vs 接口初始化

  1. 类初始化时,要求其父类全部都已经类初始化过了
  2. 接口初始化时,并不要求其父接口全部都完成初始化,只有在真正使用父接口的时候(如引用接口中定义的常量)才会进行父接口初始化
  3. 接口的实现类在进行类初始化时,也一样不会进行父接口初始化

主动引用 - 触发类初始化

new指令

代码

1
2
3
4
5
6
7
8
9
10
11
public class NewTest {
static class A {
static {
System.out.println("Class A Initialization");
}
}

public static void main(String[] args) {
new A(); // 触发类A的初始化
}
}

运行结果

1
Class A Initialization

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
# new指令会触发类A的初始化
0: new #2 // class me/zhongmingmao/class_initialization/NewTest$A
3: dup
4: invokespecial #3 // Method me/zhongmingmao/class_initialization/NewTest$A."<init>":()V
7: pop
8: return
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;

putstatic/getstatic指令

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StaticTest {
static class A {
static String name;

static {
System.out.println("Class A Initialization");
}
}

static class B {
static String name;

static {
System.out.println("Class B Initialization");
}
}

public static void main(String[] args) {
A.name = "zhongmingmao"; // 触发类A的初始化
String name = B.name; // 触发类B的初始化
}
}

运行结果

1
2
Class A Initialization
Class B Initialization

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String zhongmingmao
# putstatic指令会触发类A的初始化
2: putstatic #3 // Field me/zhongmingmao/class_initialization/StaticTest$A.name:Ljava/lang/String;
# getstatic指令会触发类B的初始化
5: getstatic #4 // Field me/zhongmingmao/class_initialization/StaticTest$B.name:Ljava/lang/String;
8: astore_1
9: return
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
9 1 1 name Ljava/lang/String;

putstatic/getstatic指令会触发类的初始化

invokestatic指令

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InvokestaticTest {
static class A {
static {
System.out.println("Class A Initialization");
}

static void staticMethod() {
}
}

public static void main(String[] args) {
A.staticMethod(); // 触发类A的初始化
}
}

运行结果

1
Class A Initialization

字节码

1
2
3
4
5
6
7
8
9
10
11
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
# invokestatic指令会触发类A的初始化
0: invokestatic #2 // Method me/zhongmingmao/class_initialization/InvokestaticTest$A.staticMethod:()V
3: return
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;

反射

Class.forName

Class.forName方法可以通过参数initialize来确定是否触发类的初始化

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ClassForNameTest {
static class A {

static {
System.out.println("Class A Initialization");
}
}

static class B {

static {
System.out.println("Class B Initialization");
}
}

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException {
ClassLoader classLoader = ClassForNameTest.class.getClassLoader();
Class.forName(A.class.getName()); // 触发类A的初始化

Class<?> clazz = Class.forName(B.class.getName(), false, classLoader); // 不会触发类B的初始化
System.out.println("Load Class B");
clazz.newInstance(); // 触发类B的初始化
}
}

运行结果

1
2
3
Class A Initialization
Load Class B # 说明此时类B尚未初始化
Class B Initialization

分析

Class.forName实际调用的是native方法(对应的CPP代码暂未研究)forName0,有一个标志位initialize,是否进行类的初始化

1
2
3
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller) throws ClassNotFoundException;

Class.newInstance

  1. ClassLoader.loadClass不会触发类的初始化
  2. Class.newInstance会触发类的初始化

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LoadClassTest {
static class A {

static {
System.out.println("Class A Initialization");
}
}

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException {
ClassLoader classLoader = LoadClassTest.class.getClassLoader();
Class<?> clazz = classLoader.loadClass(A.class.getName()); // 被动加载类A,不会触发类A的初始化
System.out.println("Load Class A");
clazz.newInstance(); // 触发类A的初始化
}
}

运行结果

1
2
Load Class A
Class A Initialization

继承

当初始化一个类的时候,如果发现其父类还没有进行过初始化,则首先触发其父类的初始化

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InheritTest {
static class Father {

static {
System.out.println("Class Father Initialization");
}
}

static class Son extends Father {

static {
System.out.println("Class Son Initialization");
}
}

public static void main(String[] args) {
new Son(); // 首先触发类Father的初始化,再触发类Son的初始化
}
}

运行结果

1
2
Class Father Initialization
Class Son Initialization

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
# new指令会触发类Son的初始化,由于其直接父类Father尚未初始化,会首先触发直接父类Father的初始化
0: new #2 // class me/zhongmingmao/class_initialization/InheritTest$Son
3: dup
4: invokespecial #3 // Method me/zhongmingmao/class_initialization/InheritTest$Son."<init>":()V
7: pop
8: return
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;

执行主类

当JVM启动时,用户需要指定一个要执行的主类,JVM会先初始化这个主类

代码

1
2
3
4
5
6
7
8
public class MainClassTest {
static {
System.out.println("Class MainClassTest Initialization");
}

public static void main(String[] args) {
}
}

运行结果

1
Class MainClassTest Initialization

被动引用 - 不触发类初始化

static 字段

  1. 对于static字段,只有**直接定义这个字段的类才会被初始化**
  2. 通过子类来引用父类中定义的static字段,只会触发父类的初始化而不会触发子类的初始化

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StaticFieldTest {
static class Father {
static String FATHER_CLASS_NAME = Father.class.getName();

static {
System.out.println("Class Father Initialization");
}
}

static class Son extends Father {
static String SON_CLASS_NAME = Son.class.getName();

static {
System.out.println("Class Son Initialization");
}
}

public static void main(String[] args) {
String className = Son.FATHER_CLASS_NAME; // 只会触发类Father的初始化,不会触发类Son的初始化
}
}

运行结果

1
Class Father Initialization

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
# getstatic指令会触发类的初始化,FATHER_CLASS_NAME在类Father中定义,因此只会触发类Father(及其父类)的初始化
0: getstatic #2 // Field me/zhongmingmao/class_initialization/StaticFieldTest$Son.FATHER_CLASS_NAME:Ljava/lang/String;
3: astore_1
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 args [Ljava/lang/String;
4 1 1 className Ljava/lang/String;
}

引用数组

通过数组定义来引用类,不会触发类的初始化

代码

1
2
3
4
5
6
7
8
9
10
11
public class ArrayRefTest {
static class A {
static {
System.out.println("Class A Initialization");
}
}

public static void main(String[] args) {
A[] as = new A[10]; // 不会触发类A的初始化
}
}

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: bipush 10
# anewarray指令并不会触发类A的初始化
2: anewarray #2 // class me/zhongmingmao/class_initialization/ArrayRefTest$A
5: astore_1
6: return
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 args [Ljava/lang/String;
6 1 1 as [Lme/zhongmingmao/class_initialization/ArrayRefTest$A;
}

编译时常量

常量编译阶段会存入Class文件常量池Constant pool)中,编译时便会**直接替换**,本质上并没有直接引用绑定到定义常量的类,不会触发定义常量的类的初始化

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConstantValueTest {
static class A {
static final String NAME = "zhongmingmao";

static {
System.out.println("Class A Initialization");
}
}

public static void main(String[] args) {
String className = A.NAME;// 不会触发类A的初始化
}
}

字节码

ConstantValueTest$A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static final java.lang.String NAME;
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String zhongmingmao # 存储在Class文件常量池,运行时会存放到运行时常量池

# clinit 方法
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Class A Initialization
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

ConstantValueTest

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
# ldc指令直接将常量直接压入操作数栈,这在编译时已经可以确定了,因此直接替换,因此在运行时也不会触发类A的初始化
0: ldc #3 // String zhongmingmao
2: astore_1
3: return
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 className Ljava/lang/String;