直面底层之字节码看类初始化

624 阅读4分钟

前言

juejin.cn/post/694834… 这篇文章最后我们讲了 new dup invokespecial 要形成条件反射是一个类的初始化,那篇文章我们就从字节码指令看下类的初始化。


一、简单的例子看类初始化

首先我们明确以下指令的含义:

new : 创建类的实例,分配内存空间,但是还没有调用init 构造方法

dup: 复制一份实例到操作数栈顶

Invokespecial: 调用构造器函数,在调用构造函数之前会调用 cinit (静态代码块的初始化)

//java
public class ClassInit {
        static int a;
        static int b;
        static {
            a = 1;
            b = 2;
        }
}

//字节码:
public class javaplan.ClassInit {
    static int a;
    static int b;

    //默认构造函数
    public javaplan.ClassInit();
    Code:
    0: aload_0   //加载局部变量表第一个到操作数栈顶 即this
    1: invokespecial #1 // Method java/lang/Object."<init>":()V //执行构造函数
    4: return

    //静态代码块 对应 cInit 即类的初始化

    static {};
    Code:
    0: iconst_1   //将int 1 加载到操作数栈顶
    1: putstatic #2 // Field a:I  //给变量a 赋值
    4: iconst_2   //将int 2 加载到操作数栈顶 
    5: putstatic #3 // Field b:I 给变量b 赋值
    8: return
}

我们看到 类的构造函数对应的了 init,静态代码块对应了 cinit


二、看一道老题

public class ClassA {
    static {
        System.out.println("A init");
    }
    public ClassA() {
        System.out.println("A Instance");
    }
}
public class ClassB extends ClassA{
    static {
        System.out.println("B init");
    }
    public ClassB () {
        System.out.println("B Instance");
    }

    public static void main(String[] args) {
        ClassA a = new ClassB();
        //ClassB [] arr = new ClassB[10]; 
    }
}

//C 
public class ClassInit {
    public static void main(String[] args) {
        ClassB[] array = new ClassB[10];
    }
}
(1).ClassB中 调用了 new ClassB(),会输出什么?
(2). main方法中 换成 ClassB [] arr = new ClassB[10] 会输出什么?
(3).新的类 ClassInit 中 调用ClassB[] array = new ClassB[10] 会输出什么?
a.我们先看第一种情况的字节码:
public class javaplan.ClassA {
//A 的构造函数 即 A的init
public javaplan.ClassA();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String A Instance
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return

//A 的cinit 即A的 类初始化,静态代码块
static {};
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String A init
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

public class javaplan.ClassB extends javaplan.ClassA {

//B的构造方法 即B init 
public javaplan.ClassB();
Code:
0: aload_0
1: invokespecial #1 // Method javaplan/ClassA."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String B Instance
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
//B 的静态main 方法
public static void main(java.lang.String[]);
Code:

// 执行new ClassB
0: new #5 // class javaplan/ClassB
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: return
  
//B 的类初始化  cinit 即B的静态代码块
static {};
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String B init
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

可以看到B的构造方法(init)中 抵用了 A类的构造方法(init),而 调用构造方法前会调用类的 cinit(静态代码块),new ClassB(),会触发 B init,而B init 会先调用 B cinit, Binit 会先调用 A init,而A的init会先调用 A cinit,所有顺序是 B cinit 、 A cinit、 A init 、B init

b. 我们看下第二种情况:
  public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: anewarray     #5                  // class javaplan/ClassB
       5: astore_1
       6: return

第二种情况只是new 了一个 B的数组,因为main 方法是 B类的 静态方法,只涉及到B类的加载,并不涉及到 ClassB 的init 方法调用,所以也不会调用 A类的cinit 和 init,所有输出结果只有 B cinit

c. 第三中情况 我们看下 classinit 的字节码:
 public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: anewarray     #2                  // class javaplan/ClassB
       5: astore_1
       6: return

由于main 方法属于 ClassInit,new 一个数组只涉及分配内存,并不会触发 B的初始化,所以什么也不会输出。


三、看一个问题

public class ClassInit {
    int a;
    public  void main(String[] args) {
        int b;
        System.out.println(a);
        System.out.println(b);
    }
}

如上述代码,编译器告诉我们 b 未初始化,会报错,那么为什么a 不会报错呢?

这就需要我们了解类的加载过程

  • 类加载校验
  • 执行 static 代码块 即 cinit
  • 为对象分配堆内存
  • 对成员变量进行初始化(对象的实例字段在可以不赋初始值就直接使用,而局部变量中如果不赋值就直接使用,因为没有这一步操作,不赋值是属于未定义的状态,编译器会直接报错)
  • 调用初始化代码块 {} 内的代码
  • 调用构造器函数 init

有了上述流程,也就是 成员变量的初始化在 构造函数之前,但局部变量如果不赋值,就属于未定义状态,则不可用。


四、总结

New 一个类的执行顺序:静态代码块(cinit)、初始化代码块({}内的代码)、构造函数(init)