Java 新建对象过程分析

1. 写在前面


“[JVM 解剖公园][1]”是一个持续更新的系列迷你博客,阅读每篇文章一般需要5到10分钟。限于篇幅,仅对某个主题按照问题、测试、基准程序、观察结果深入讲解。因此,这里的数据和讨论可以当轶事看,不做写作风格、句法和语义错误、重复或一致性检查。如果选择采信文中内容,风险自负。


Aleksey Shipilёv,JVM 性能极客   

推特 [@shipilev][2]   

问题、评论、建议发送到 [aleksey@shipilev.net][3]


[1]:https://shipilev.net/jvm-anatomy-park

[2]:http://twitter.com/shipilev

[3]:aleksey@shipilev.net


2. 问题


听说分配与初始化不同。Java 有构造函数,它究竟会执行分配还是做初始化呢?


3. 理论


如果打开 [GC Handbook][4],它会告诉你创建一个新对象通常包括三个阶段:


> 译注:GC Handbook 中文版《垃圾回收算法手册》


  1. "分配":从进程空间中分配实例数据。

  2. "系统初始化":按照 Java 语言规范进行初始化。在 C 语言中,分配新对象不需要初始化;在 Java 中,所有新创建的对象都要进行系统初始化赋默认值,设置完整的对象头等等。

  3. "二次初始化(用户初始化)":执行与该对象类型关联的所有初始化语句和构造函数。


在前面 [TLAB 分配][5]中我们对此进行过讨论,现在介绍详细的初始化过程。假如你熟悉 Java 字节码,就会知道 `new` 语句对应了几条字节码指令。例如:


```java
public Object t() {
 return new Object();
}
```


会编译为:


```java
 public java.lang.Object t();
   descriptor: ()Ljava/lang/Object;
   flags: (0x0001) ACC_PUBLIC
   Code:
     stack=2, locals=1, args_size=1
        0: new           #4                  // java/lang/Object 类
        3: dup
        4: invokespecial #1                  // java/lang/Object."<init>":()V 方法
        7: areturn
```


[4]:http://gchandbook.org/

[5]:https://shipilev.net/jvm/anatomy-quarks/4-tlab-allocation/


看起来 `new` 会执行分配和系统初始化,同时调用构造函数(`<init>`)执行用户初始化。然而,智能的 Hotspot 虚拟机会不会优化?比如在构造函数执行完成以前查看对象使用情况,优化可以合并的任务。接下来,让我们做个实验。


4. 实验


要解除这个疑问,可以编写下面这样的测试。初始化两个不同的类,每个类只包含一个 `int` 属性:


```java
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class UserInit {

   @Benchmark
   public Object init() {
       return new Init(42);
   }

   @Benchmark
   public Object initLeaky() {
       return new InitLeaky(42);
   }

   static class Init {
       private int x;
       public Init(int x) {
           this.x = x;
       }
   }

   static class InitLeaky {
       private int x;
       public InitLeaky(int x) {
           doSomething();
           this.x = x;
       }

       @CompilerControl(CompilerControl.Mode.DONT_INLINE)
       void doSomething() {
           // 此处留白
       }
   }
}
```


设计测试时,为防止编译器对 `doSomething()` 空方法进行内联优化加上了限制,迫使优化程序认为接下来可能有代码访问 `x`。换句话说,这样就无法判断 `doSomething()` 是否真的泄露了对象,从而可以有效地把对象暴露给某些外部代码。


建议启用 `-XX:+UseParallelGC -XX:-TieredCompilation -XX:-UseBiasedLocking` 参数运行测试,这样生成的代码更容易理解。JMH `-prof perfasm` 参数可以完美地转储测试生成的代码。


下面是 `Init` 测试结果:


```asm
0x00007efdc466d4cc: mov    0x60(%r15),%rax          ; 下面是 TLAB 分配
0x00007efdc466d4d0: mov    %rax,%r10
0x00007efdc466d4d3: add    $0x10,%r10
0x00007efdc466d4d7: cmp    0x70(%r15),%r10
0x00007efdc466d4db: jae    0x00007efdc466d50a
0x00007efdc466d4dd: mov    %r10,0x60(%r15)
0x00007efdc466d4e1: prefetchnta 0xc0(%r10)
                                                 ; ------- /分配 ---------
                                                 ; ------- 系统初始化 ---------
0x00007efdc466d4e9: movq   $0x1,(%rax)              ; header 设置 mark word
0x00007efdc466d4f0: movl   $0xf8021bc4,0x8(%rax)    ; header 设置 class word
                                                 ; ...... 系统/用户初始化 .....
0x00007efdc466d4f7: movl   $0x2a,0xc(%rax)          ; x = 42.
                                                 ; -------- /用户初始化 ---------
```


上面生成的代码中可以看到 TLAB 分配、对象元数据初始化,然后对字段执行系统+用户初始化。`InitLeaky` 的测试结果有很大区别:


```asm
                                                 ; ------- 分配 ----------
0x00007fc69571bf4c: mov    0x60(%r15),%rax
0x00007fc69571bf50: mov    %rax,%r10
0x00007fc69571bf53: add    $0x10,%r10
0x00007fc69571bf57: cmp    0x70(%r15),%r10
0x00007fc69571bf5b: jae    0x00007fc69571bf9e
0x00007fc69571bf5d: mov    %r10,0x60(%r15)
0x00007fc69571bf61: prefetchnta 0xc0(%r10)
                                                 ; ------- /分配 ---------
                                                 ; ------- 系统初始化 ---------
0x00007fc69571bf69: movq   $0x1,(%rax)              ; header 设置 mark word
0x00007fc69571bf70: movl   $0xf8021bc4,0x8(%rax)    ; header 设置 class word
0x00007fc69571bf77: mov    %r12d,0xc(%rax)          ; x = 0 (%r12 的值恰好是 0)
                                                 ; ------- /系统初始化 --------
                                                 ; -------- 用户初始化 ----------
0x00007fc69571bf7b: mov    %rax,%rbp
0x00007fc69571bf7e: mov    %rbp,%rsi
0x00007fc69571bf81: xchg   %ax,%ax
0x00007fc69571bf83: callq  0x00007fc68e269be0       ; call doSomething()
0x00007fc69571bf88: movl   $0x2a,0xc(%rbp)          ; x = 42
                                                 ; ------ /用户初始化 ------
```


由于优化程序无法确定是否需要 `x` 值,因此这里必须假定出现最坏的情况,先执行系统初始化,然后再完成用户初始化。


5. 观察


虽然教科书的定义很完美,而且生成的字节码也提供了佐证,但只要不出现奇怪的结果,优化程序还是会做一些不为人知的优化。从编译器的角度看,这只是一种简单优化。但从概念上说,这个结果已经超出了“阶段”的范畴。


上一篇:Java 新建对象过程分析


下一篇:当我谈 Rax 按端拆分代码的时候我谈些什么