# Java <clint>
与 <init>
原理
上周一个 一十斤 的学生来问了一个 Java 关于 init 、clint 的问题,想来是因为 他花了 一万多 报了名,我离职一年多了,就没有回答其问题,因为该问题说简单,也简单,说难也难,于是就让他咨询了 一十斤 那边的 资深讲师,结果耗时 3 天 给了个 模棱两可,随意糊弄的答案。
由于该问题作为一道面试题(这位学生说的遇到的面试),也属于 Java 和 JVM 执行的基础问题,所以本文将详细描述 init 、 clint 以及 JVM 中的处理过程。
# 描述
看如下 java 代码,该代码来自于这位学生的原问题。
public class Demo {
public static Demo demo = new Demo();
public static int value1;
public static int value2 = 2;
public Demo() {
value1++;
value2++;
}
public static void main(String[] args) throws Exception {
System.out.println(value1);
System.out.println(value2);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出结果为:
1
2
2
该代码非常简单,三个静态变量:
1、一个Demo对象
2、一个静态变量 value1 没有赋值
3、一个静态变量 value2 赋值为2
那么,对象构造器中对 value1 和 value2 分别自增,那么为啥 value1 的自增值保留下来了,但 value2 的值却没有呢?
如果读者有这样的疑问,那么证明对于 Java 静态初始化和对象初始化过程还是不了解。让我们修改下程序:
public class Demo {
public static Demo demo = new Demo();
public static int value1 = 0; // 看这里,赋值为了 0
public static int value2 = 2;
public Demo() {
value1++;
value2++;
}
public static void main(String[] args) throws Exception {
System.out.println(value1);
System.out.println(value2);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
输出结果:
0
2
2
可以看到,最终 value1 和 value2 变量的自增值没了,何故?其实看出来了,是因为 静态初始化的顺序:demo、value1、value2。demo 对象构建时在 构造函数中确实 对 value1 和 value2 变量自增了,但由于 value1和value2的初始化顺序在 demo对象之后,所以值被覆盖了。
那么为什么第一个例子没有被覆盖呢?因为我们没有显示对 value1 赋值 ----- 只有显示赋值 才会覆盖。
# 字节码
那么,问题很容易就分析清楚了,但是原理呢?我们先来看第一个程序的字节码。我们看到在 静态代码块中 并没有出现 value1 的变量值 赋值,说明:如果不显示赋值,那么并不会在 static代码块中 出现 写入变量的代码,同时写入 static 代码块中的 顺序 按照程序编写顺序生成字节码。
public Demo(); // Demo 的构造器
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field value1:I
7: iconst_1
8: iadd
9: putstatic #2 // Field value1:I value1 自增值写入
12: getstatic #3 // Field value2:I value2 自增值写入
15: iconst_1
16: iadd
17: putstatic #3 // Field value2:I
20: return
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #6 // class Demo
3: dup
4: invokespecial #7 // Method "<init>":()V
7: putstatic #8 // Field demo:LDemo;
10: iconst_2
11: putstatic #3 // Field value2:I value2 显示值 2 写入
14: return
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
28
29
接下来我们来看看第二个程序的字节码。很明显了对吧?
public Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field value1:I
7: iconst_1
8: iadd
9: putstatic #2 // Field value1:I
12: getstatic #3 // Field value2:I
15: iconst_1
16: iadd
17: putstatic #3 // Field value2:I
20: return
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #6 // class Demo
3: dup
4: invokespecial #7 // Method "<init>":()V
7: putstatic #8 // Field demo:LDemo;
10: iconst_0
11: putstatic #2 // Field value1:I value1 显示值 0 写入
14: iconst_2
15: putstatic #3 // Field value2:I value2 显示值 2 写入
18: return
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
28
29
30
31
# JVM 层面
那么到字节码层面就结束了?怎么验证 默认静态变量、 static 静态块 和 demo 构造块的执行顺序呢?看如下C++代码。 JVM 在加载字节码时将会调用 parseClassFile 方法,可以看到在该方法中将初始化 静态变量的默认值。
instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
ClassLoaderData* loader_data,
Handle protection_domain,
KlassHandle host_klass,
GrowableArray<Handle>* cp_patches,
TempNewSymbol& parsed_name,
bool verify,
TRAPS) {
...
constantPoolHandle cp = parse_constant_pool(CHECK_(nullHandle)); // 解析常量池
...
instanceKlassHandle super_klass = parse_super_class(super_class_index,
CHECK_NULL); // 解析父类
...
Array<Klass*>* local_interfaces =
parse_interfaces(itfs_len, protection_domain, _class_name,
&has_default_methods, CHECK_(nullHandle)); // 解析接口
...
Array<u2>* fields = parse_fields(class_name,
access_flags.is_interface(),
&fac, &java_fields_count,
CHECK_(nullHandle)); // 解析属性
...
Array<Method*>* methods = parse_methods(access_flags.is_interface(),
&promoted_flags,
&has_final_method,
&has_default_methods,
CHECK_(nullHandle)); // 解析方法
...
_klass = InstanceKlass::allocate_instance_klass(loader_data,
vtable_size,
itable_size,
info.static_field_size,
total_oop_map_size2,
rt,
access_flags,
name,
super_klass(),
!host_klass.is_null(),
CHECK_(nullHandle)); // 创建 klass 类表示解析后的类信息
...
java_lang_Class::create_mirror(this_klass, protection_domain, CHECK_(nullHandle)); // 创建 java 层面的 Class 对象,并在其中初始化静态变量(读者可以打开看 initialize_static_field 方法即可,这里面将默认的静态变量初始化为初始值,对应于本例子,将 value1 和 value2 初始化为 0 )
...
}
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
那么,当我们使用类时将会调用 InstanceKlass::initialize 方法初始化类,可以看到在其中调用了 clint 方法,也即 static 代码块。
void InstanceKlass::initialize(TRAPS) {
if (this->should_be_initialized()) {
HandleMark hm(THREAD);
instanceKlassHandle this_oop(THREAD, this);
initialize_impl(this_oop, CHECK);
} else {
assert(is_initialized(), "sanity check");
}
}
void InstanceKlass::initialize_impl(instanceKlassHandle this_oop, TRAPS) {
...
this_oop->call_class_initializer(THREAD); // 调用 clint (也即上述的 static 块)
}
void InstanceKlass::call_class_initializer(TRAPS) {
instanceKlassHandle ik (THREAD, this);
call_class_initializer_impl(ik, THREAD);
}
void InstanceKlass::call_class_initializer_impl(instanceKlassHandle this_oop, TRAPS) {
...
methodHandle h_method(THREAD, this_oop->class_initializer()); // 封装 static 代码方法对象(也即 clint 方法)
...
JavaCalls::call(&result, h_method, &args, CHECK); // 调用 static 代码块
}
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
# 总结
从上述分析中,我们很容易看到如下顺序:
1、JVM 在加载类中 首先将静态变量默认初始化为默认值
2、JVM 在后续使用类时,将会初始化类,在初始化过程中,调用 clinit 方法,该方法将执行 字节码中的 static代码块
这时,我们可以以此来解释程序 1 的结果:
1、Javac 编译器 将 value2 的显示赋值操作 生成字节码保存在 static块中,由于 value1 没有显示赋值,所以没有生成设置 value1 的代码
2、JVM 加载类时,将value1 和 value2 初始化为默认值 0
3、JVM 使用类时,按顺序执行 clinit 方法,该方法 按顺序执行字节码。第一个字节码时创建 demo对象,然后调用 对象的 init 构造器 将value1 和value2 的 0 值 自增为1,然后调用 Javac 生成的 显示设置 value2 值的字节码 将 自增后 的 1 覆盖为 2。而又由于 Javac 没有生成 对 value1的赋值操作(由于没有显示赋值),所以 自增的 1 得以保留下来。
最后附上那边给的答案吧。