歡迎光臨
每天分享高質量文章

Java 新建物件過程分析

(給ImportNew加星標,提高Java技能)

 

編譯:ImportNew/唐尤華

shipilev.net/jvm/anatomy-quarks/6-new-object-stages/

 

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."":()V 方法
         7: areturn
```

 

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

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

 

看起來 `new` 會執行分配和系統初始化,同時呼叫建構式(“)執行使用者初始化。然而,智慧的 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)              ; essay-header 設定 mark word
0x00007efdc466d4f0: movl   $0xf8021bc4,0x8(%rax)    ; essay-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)              ; essay-header 設定 mark word
0x00007fc69571bf70: movl   $0xf8021bc4,0x8(%rax)    ; essay-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. 觀察

 

雖然教科書的定義很完美,而且生成的位元組碼也提供了佐證,但只要不出現奇怪的結果,最佳化程式還是會做一些不為人知的最佳化。從編譯器的角度看,這隻是一種簡單最佳化。但從概念上說,這個結果已經超出了“階段”的範疇。

贊(0)

分享創造快樂