类对象与实例对象的构建

无论是新建类对象还是实例对象都需要经过对应的构造器,类对象构建器在字节码层面表示为 clinit,实例对象在字节码层面表示为 init。

两种构造方式都提供了线程安全机制:

  • 类对象的 clinit 构造器的执行 JVM 通过 synchronized 同步代码块方式保证多线程情况下被正确的执行,并且仅执行一次。
  • 实例对象的 init 构造器方式通过 TLAB 在堆中以线程来划分预先占用内存空间方式达到线程隔离(分配后使用时线程共享的)。

new 对象的创建流程

用 Java 实现单例模式的多种写法都是为了在线程线程安全的前提下尽量缩小锁的范围。原因就是 new 关键字对应于字节码的多条指令,可能由于线程切换从而导致程序处于不一致或错误的状态。

如下代码:

TestDemo testDemo = new TestDemo();

对应的字节码如下:

 Code:
       0: new           #2                  // class com/linewell/license/cloud/util/TestDemo
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1

在字节码层面创建对象对应于三条指令:new、dup、invokespecial,astore_1 是将创建好的对应引用存入局部变量表下标为 1 的位置。

在字节码层面<init>将把对象的所有实例字段、实例代码块按顺序收集起来,再把对象实例的构造函数里的初始化逻辑放在<init>函数的最后。所以<init>构造函数包含了堆中每个对象的初始化逻辑。

new 关键字对应了 3 条指令。第 1 条 new 指令只是在堆中创建了一个对象实例,此时对象的所有字段还都是默认值(int 为 0,float 为 0.0f),这个和类加载的准备阶段类似。在第 3 条指令 invokespecial 调用<init>函数的指令执行后,才能将创建的对象按照 Java 代码进行初始化赋上对应的值。

在 invokespecial 指令执行后,操作数栈栈顶的引用元素将伴随着出栈,如果不对引用进行备份将找不回刚才创建对象对应的内存地址,而 dup 的指令正是对操作数栈栈顶的引用元素进行复制然后将复制后的元素再次压入栈中。如下图:

clinit 构造器的调用

clinit 调用机制在类加载的初始化阶段,比 init 更早,这个构造器的调用由 4 条指令触发:new(创建实例对象)、getstatic(访问静态变量或静态方法)、putstatic(设置静态变量)、invokestatic(调用静态方法)。

如下代码:

public class TestDemo {
   static int a;
   static int b;
   static {
      a = 1;
      b = 2;
   }
}

对应的字节码中的 clinit 构造器:

 static {};
    Code:
       0: iconst_1
       1: putstatic     #2                  // Field a:I
       4: iconst_2
       5: putstatic     #3                  // Field b:I
       8: return

init 与 clinit 调用链

如下代码:

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

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

其中类 B 对应的字节码如下:

public com.linewell.license.cloud.util.B();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method com/linewell/license/cloud/util/A."<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

  static {};
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #5                  // String B init
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

可以看到在执行 B 类的 new 后,先执行 A 类的<init>实例初始化,在实例化前发现 B 类还没进行静态初始化(<clinit>)所以会先执行 B 类的静态初始化,在执行前发现父类 A 类也没有静态初始化,所以会先执行 A 类的静态初始化再执行 B 类的静态初始化。然后在执行 B 类的<init>实例初始化前先执行 A 类的实例初始化。所以流程如下图,数字代表执行步骤,箭头是调用和触发步骤:

  1. B<init> 执行前触发 B<clinit>
  2. 如果 A<clinit>还没执行,先执行 A<clinit>再执行 B<clinit>
  3. B<init>调用前先调用 A<init>
  4. 最后执行 B<init>

参考

最后修改日期: 2019年12月25日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。