# 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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出结果为:

1
2
1
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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出结果:

0
2
1
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
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
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: returnstatic {};
    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
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
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 )
   ...
}
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
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 代码块
}
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

# 总结

从上述分析中,我们很容易看到如下顺序:

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 得以保留下来。

最后附上那边给的答案吧。

img