从字节码层面彻底弄清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的子类)

从字节码层面彻底弄清Java类的初始化顺序

好了,现在是不是清楚多了。原来在字节码层面根本就没有那么多复制概念,什么静态构造快,实例构造快,构造方法等等,在这里只有两个方法类的初始化方法<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类的初始化顺序。

参考:JVM 字节码从入门到精通