从字节码层面彻底弄清Java类的初始化顺序
这里先说结论:对于没有继承关系的类,初始化顺序为:静态变量->静态初始化块->实例变量->实例初始化块->构造器。(静态变量与静态初始化块的初始化只会执行一次)。
对于有继承关系的类,初始化顺序为:父类静态变量->父类静态初始化块->子类静态变量->子类静态初始化块->父类实例变量->父类实例初始化块->父类构造方法->子类实例变量->子类实例初始化块->子类构造器。(同样父类与子类的静态变量与静态初始化块的初始化只会执行一次)。
下面我们从字节码的层面来分析这一执行流程,这里只分析比较复杂的一种情况,即有继承关系的,里面还涉及到多态,希望大家不要晕呀。
我们分析的源码(Son.java)如下:
class Father {
private int i = test();
private static int j = method();
static {
System.out.print("(1)");
}
Father() {
System.out.print("(2)");
}
{
System.out.print("(3)");
}
public int test() {
System.out.print("(4)");
return 1;
}
public static int method() {
System.out.print("(5)");
return 1;
}
}
public class Son extends Father {
private int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
Son() {
System.out.print("(7)");
}
{
System.out.print("(8)");
}
public int test() {
System.out.print("(9)");
return 1;
}
public static int method() {
System.out.print("(10)");
return 1;
}
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
首先使用javac Son.java 编译原文件,得到两个class文件,分别为Father.class与Son.class。
接着使用javap -l -v -p Father.class > Father.txt 与 javap -l -v -p Son.class > Son.txt 得到 Father类与Son类的字节码文件。
Father.txt:
Classfile /C:/Users/CL/Desktop/Father.class
Last modified 2019-4-11; size 663 bytes
MD5 checksum 404517edd18731f02f797ae96046af13
Compiled from "Son.java"
class Father
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #14.#28 // java/lang/Object."<init>":()V
#2 = Methodref #13.#29 // Father.test:()I
#3 = Fieldref #13.#30 // Father.i:I
#4 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #33 // (3)
#6 = Methodref #34.#35 // java/io/PrintStream.print:(Ljava/lang/String;)V
#7 = String #36 // (2)
#8 = String #37 // (4)
#9 = String #38 // (5)
#10 = Methodref #13.#39 // Father.method:()I
#11 = Fieldref #13.#40 // Father.j:I
#12 = String #41 // (1)
#13 = Class #42 // Father
#14 = Class #43 // java/lang/Object
#15 = Utf8 i
#16 = Utf8 I
#17 = Utf8 j
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 test
#23 = Utf8 ()I
#24 = Utf8 method
#25 = Utf8 <clinit>
#26 = Utf8 SourceFile
#27 = Utf8 Son.java
#28 = NameAndType #18:#19 // "<init>":()V
#29 = NameAndType #22:#23 // test:()I
#30 = NameAndType #15:#16 // i:I
#31 = Class #44 // java/lang/System
#32 = NameAndType #45:#46 // out:Ljava/io/PrintStream;
#33 = Utf8 (3)
#34 = Class #47 // java/io/PrintStream
#35 = NameAndType #48:#49 // print:(Ljava/lang/String;)V
#36 = Utf8 (2)
#37 = Utf8 (4)
#38 = Utf8 (5)
#39 = NameAndType #24:#23 // method:()I
#40 = NameAndType #17:#16 // j:I
#41 = Utf8 (1)
#42 = Utf8 Father
#43 = Utf8 java/lang/Object
#44 = Utf8 java/lang/System
#45 = Utf8 out
#46 = Utf8 Ljava/io/PrintStream;
#47 = Utf8 java/io/PrintStream
#48 = Utf8 print
#49 = Utf8 (Ljava/lang/String;)V
{
private int i;
descriptor: I
flags: ACC_PRIVATE
private static int j;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC
Father();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_0
6: invokevirtual #2 // Method test:()I
9: putfield #3 // Field i:I
12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #5 // String (3)
17: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
20: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
23: ldc #7 // String (2)
25: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 9: 0
line 2: 4
line 14: 12
line 10: 20
line 11: 28
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String (4)
5: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: iconst_1
9: ireturn
LineNumberTable:
line 18: 0
line 19: 8
public static int method();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #9 // String (5)
5: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: iconst_1
9: ireturn
LineNumberTable:
line 23: 0
line 24: 8
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #10 // Method method:()I
3: putstatic #11 // Field j:I
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #12 // String (1)
11: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
14: return
LineNumberTable:
line 3: 0
line 6: 6
line 7: 14
}
SourceFile: "Son.java"
Son.txt :
Classfile /C:/Users/CL/Desktop/Son.class
Last modified 2019-4-11; size 781 bytes
MD5 checksum a450815a8a270feac5cc878c2536985a
Compiled from "Son.java"
public class Son extends Father
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #16.#32 // Father."<init>":()V
#2 = Methodref #10.#33 // Son.test:()I
#3 = Fieldref #10.#34 // Son.i:I
#4 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #37 // (8)
#6 = Methodref #38.#39 // java/io/PrintStream.print:(Ljava/lang/String;)V
#7 = String #40 // (7)
#8 = String #41 // (9)
#9 = String #42 // (10)
#10 = Class #43 // Son
#11 = Methodref #10.#32 // Son."<init>":()V
#12 = Methodref #38.#44 // java/io/PrintStream.println:()V
#13 = Methodref #10.#45 // Son.method:()I
#14 = Fieldref #10.#46 // Son.j:I
#15 = String #47 // (6)
#16 = Class #48 // Father
#17 = Utf8 i
#18 = Utf8 I
#19 = Utf8 j
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 test
#25 = Utf8 ()I
#26 = Utf8 method
#27 = Utf8 main
#28 = Utf8 ([Ljava/lang/String;)V
#29 = Utf8 <clinit>
#30 = Utf8 SourceFile
#31 = Utf8 Son.java
#32 = NameAndType #20:#21 // "<init>":()V
#33 = NameAndType #24:#25 // test:()I
#34 = NameAndType #17:#18 // i:I
#35 = Class #49 // java/lang/System
#36 = NameAndType #50:#51 // out:Ljava/io/PrintStream;
#37 = Utf8 (8)
#38 = Class #52 // java/io/PrintStream
#39 = NameAndType #53:#54 // print:(Ljava/lang/String;)V
#40 = Utf8 (7)
#41 = Utf8 (9)
#42 = Utf8 (10)
#43 = Utf8 Son
#44 = NameAndType #55:#21 // println:()V
#45 = NameAndType #26:#25 // method:()I
#46 = NameAndType #19:#18 // j:I
#47 = Utf8 (6)
#48 = Utf8 Father
#49 = Utf8 java/lang/System
#50 = Utf8 out
#51 = Utf8 Ljava/io/PrintStream;
#52 = Utf8 java/io/PrintStream
#53 = Utf8 print
#54 = Utf8 (Ljava/lang/String;)V
#55 = Utf8 println
{
private int i;
descriptor: I
flags: ACC_PRIVATE
private static int j;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC
Son();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method Father."<init>":()V
4: aload_0
5: aload_0
6: invokevirtual #2 // Method test:()I
9: putfield #3 // Field i:I
12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #5 // String (8)
17: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
20: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
23: ldc #7 // String (7)
25: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
28: return
LineNumberTable:
line 36: 0
line 29: 4
line 41: 12
line 37: 20
line 38: 28
public int test();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #8 // String (9)
5: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: iconst_1
9: ireturn
LineNumberTable:
line 45: 0
line 46: 8
public static int method();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #9 // String (10)
5: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
8: iconst_1
9: ireturn
LineNumberTable:
line 50: 0
line 51: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #10 // class Son
3: dup
4: invokespecial #11 // Method "<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: invokevirtual #12 // Method java/io/PrintStream.println:()V
14: new #10 // class Son
17: dup
18: invokespecial #11 // Method "<init>":()V
21: astore_2
22: return
LineNumberTable:
line 55: 0
line 56: 8
line 57: 14
line 58: 22
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #13 // Method method:()I
3: putstatic #14 // Field j:I
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #15 // String (6)
11: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
14: return
LineNumberTable:
line 30: 0
line 33: 6
line 34: 14
}
SourceFile: "Son.java"
如果大家看到了这里,请将上面的两个字节码文件复制保存下来查阅,下面会频繁的用到这些字节码,不然一直往上翻会影响阅读。
好,现在开始分析:
当然是从main函数开始分析了:
我们看到Son.txt中main函数的字节码为:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #10 // class Son
3: dup
4: invokespecial #11 // Method "<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: invokevirtual #12 // Method java/io/PrintStream.println:()V
14: new #10 // class Son
17: dup
18: invokespecial #11 // Method "<init>":()V
21: astore_2
22: return
最前面三条操作码,new、dup、invokespecial是使用new创建一个对象的标配。invokespecial用来调用对象的特殊方法,比如类的构造方法,私有方法和父类方法。这里表示调用类的构造方法<init>了。
由于此时是第一次使用到这个类,所以会先去调用类的静态初始化代码<clinit>(从Son.txt的常量池第29项可以看到<clinit>)。<clinit> 不会直接被调用,它在下面这个四个指令触发调用:new, getstatic, putstatic or invokestatic
。也就是说,初始化一个类实例、访问一个静态变量或者一个静态方法,类的静态初始化方法就会被触发。<clinit>对应的就是字节码中的static{};中的操作。当有继承关系时,如果父类没有被加载,会先执行父类的静态初始化,然后再执行子类的静态初始化。
它们之间的关系如下(下图中A是B的子类)
好了,现在是不是清楚多了。原来在字节码层面根本就没有那么多复制概念,什么静态构造快,实例构造快,构造方法等等,在这里只有两个方法类的初始化方法<init>与类的静态初始化方法<clinit>。
好了,现在我们知道了,类的静态初始化顺序是:父类的<clinit>->子类的<clinit>。
具体到我们的例子,我们看Father.txt中<clinit>是怎样的:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: invokestatic #10 // Method method:()I
3: putstatic #11 // Field j:I
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #12 // String (1)
11: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
14: return
可以看到,类的静态字段和静态代码块都包含在了<clinit>中,且静态字段的初始化在前,然后才是静态代码块,子类的<clinit>也是一样的。所以到这里我们就明白了父类静态变量->父类静态初始化块->子类静态变量->子类静态初始化块。
接着我们来看子类<init>的字节码:
Son();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method Father."<init>":()V
4: aload_0
5: aload_0
6: invokevirtual #2 // Method test:()I
9: putfield #3 // Field i:I
12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #5 // String (8)
17: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
20: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
23: ldc #7 // String (7)
25: invokevirtual #6 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
28: return
注意看序号为1的操作码,1: invokespecial #1 // Method Father."<init>":()V,表示调用了父类的<init>方法。所以子类的字节码中总是先调用父类的<init>方法。接下来我们看看<init>方法中,包含了那些内容。字节码6-9执行的是实例变量i的赋值。12-17执行的是实例构造快中的方法,20-25执行的是构造方法。父类的<init>方法与此类似。
好了,我们总结一下:1、如果类还没加载,会先执行类的静态初始化代码<clinit>,类的静态初始化代码包括静态实例变量的赋值与静态构造快。2、初始化类执行的是<init>方法,在<init>方法内会先调用父类的<init>方法,<init>方法内包括,实例变量的赋值与实例构造快已经构造方法。
这就是字节码层面上Java类的初始化顺序。