(給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 中文版《垃圾回收演演算法手冊》
- “分配”:從行程空間中分配實體資料。
- “系統初始化”:按照 Java 語言規範進行初始化。在 C 語言中,分配新物件不需要初始化;在 Java 中,所有新建立的物件都要進行系統初始化賦預設值,設定完整的物件頭等等。
- “二次初始化(使用者初始化)”:執行與該物件型別關聯的所有初始化陳述句和建構式。
在前面 [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. 觀察
雖然教科書的定義很完美,而且生成的位元組碼也提供了佐證,但只要不出現奇怪的結果,最佳化程式還是會做一些不為人知的最佳化。從編譯器的角度看,這隻是一種簡單最佳化。但從概念上說,這個結果已經超出了“階段”的範疇。
朋友會在“發現-看一看”看到你“在看”的內容