天天看點

Java中的對象都是在堆上配置設定的嗎?

為了防止歧義,可以換個說法:

Java對象執行個體和數組元素都是在堆上配置設定記憶體的嗎?

答:不一定。滿足特定條件時,它們可以在(虛拟機)棧上配置設定記憶體。

Java中的對象都是在堆上配置設定的嗎?

JVM記憶體結構很重要,多多複習

這和我們平時的了解可能有些不同。虛拟機棧一般是用來存儲基本資料類型、引用和傳回位址的,怎麼可以存儲執行個體資料了呢?

這是因為Java JIT(just-in-time)編譯器進行的兩項優化,分别稱作逃逸分析(escape analysis)和标量替換(scalar replacement)。

Java中的對象都是在堆上配置設定的嗎?

注意看一下JIT的位置

中文維基上對逃逸分析的描述基本準确,摘錄如下:

在編譯程式優化理論中,逃逸分析是一種确定指針動态範圍的方法——分析在程式的哪些地方可以通路到指針。當一個變量(或對象)在子程式中被配置設定時,一個指向變量的指針可能逃逸到其它執行線程中,或是傳回到調用者子程式。

如果一個子程式配置設定一個對象并傳回一個該對象的指針,該對象可能在程式中被通路到的地方無法确定——這樣指針就成功“逃逸”了。如果指針存儲在全局變量或者其它資料結構中,因為全局變量是可以在目前子程式之外通路的,此時指針也發生了逃逸。

逃逸分析确定某個指針可以存儲的所有地方,以及确定能否保證指針的生命周期隻在目前程序或線程中。

簡單來講,JVM中的逃逸分析可以通過分析對象引用的使用範圍(即動态作用域),來決定對象是否要在堆上配置設定記憶體,也可以做一些其他方面的優化。

關于逃逸分析,大家可以看下這篇文章:面試問我 Java 逃逸分析,瞬間被秒殺了。以下的例子說明了一種對象逃逸的可能性。

static StringBuilder getStringBuilder1(String a, String b) {
    StringBuilder builder = new StringBuilder(a);
    builder.append(b);
    return builder; // builder通過方法傳回值逃逸到外部
}

static String getStringBuilder2(String a, String b) {
    StringBuilder builder = new StringBuilder(a);
    builder.append(b);
    return builder.toString(); // builder範圍維持在方法内部,未逃逸
}      

以JDK 1.8為例,可以通過設定JVM參數-XX:+DoEscapeAnalysis、-XX:-DoEscapeAnalysis來開啟或關閉

逃逸分析

(預設當然是開啟的)。

下面先寫一個沒有對象逃逸的例子。

public class EscapeAnalysisTest {
  public static void main(String[] args) throws Exception {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 5000000; i++) {
      allocate();
    }
    System.out.println((System.currentTimeMillis() - start) + " ms");
    Thread.sleep(600000);
  }

  static void allocate() {
    MyObject myObject = new MyObject(2019, 2019.0);
  }

  static class MyObject {
    int a;
    double b;

    MyObject(int a, double b) {
      this.a = a;
      this.b = b;
    }
  }
}      

然後通過開啟和關閉DoEscapeAnalysis開關觀察不同。

關閉逃逸分析

~ java -XX:-DoEscapeAnalysis EscapeAnalysisTest
76 ms
~ jmap -histo 26031
 num #instances #bytes class name
----------------------------------------------
   1: 5000000      120000000  me.lmagics.EscapeAnalysisTest$MyObject
   2: 636       12026792  [I
   3: 3097        1524856  [B
   4: 5088         759960  [C
   5: 3067          73608  java.lang.String
   6: 623          71016  java.lang.Class
   7: 727          43248  [Ljava.lang.Object;
   8: 532          17024  java.io.File
   9: 225          14400  java.net.URL
  10: 334          13360  java.lang.ref.Finalizer
# ......      

開啟逃逸分析

~ java -XX:+DoEscapeAnalysis EscapeAnalysisTest
4 ms
~ jmap -histo 26655
 num #instances #bytes class name
----------------------------------------------
   1: 592       11273384  [I
   2: 90871        2180904  me.lmagics.EscapeAnalysisTest$MyObject
   3: 3097        1524856  [B
   4: 5088         759952  [C
   5: 3067          73608  java.lang.String
   6: 623          71016  java.lang.Class
   7: 727          43248  [Ljava.lang.Object;
   8: 532          17024  java.io.File
   9: 225          14400  java.net.URL
  10: 334          13360  java.lang.ref.Finalizer
# ......      

可見,關閉逃逸分析之後,堆上有5000000個MyObject執行個體,而開啟逃逸分析之後,就隻剩下90871個執行個體了,不管是執行個體數還是記憶體占用都隻有原來的2%不到。

另外,如果把堆記憶體限制得小一點(比如加上-Xms10m -Xmx10m),并且列印GC日志(-XX:+PrintGCDetails)的話,關閉逃逸分析還會造成頻繁的GC,開啟逃逸分析就沒有這種情況。這說明逃逸分析确實降低了堆記憶體的壓力。

但是,逃逸分析隻是棧上記憶體配置設定的前提,接下來還需要進行标量替換才能真正實作。

所謂标量,就是指JVM中無法再細分的資料,比如int、long、reference等。相對地,能夠再細分的資料叫做聚合量。

仍然考慮上面的例子,MyObject就是一個聚合量,因為它由兩個标量a、b組成。通過逃逸分析,JVM會發現myObject沒有逃逸出allocate()方法的作用域,标量替換過程就會将myObject直接拆解成a和b,也就是變成了:

static void allocate() {
    int a = 2019;
    double b = 2019.0;
}      

可見,對象的配置設定完全被消滅了,而int、double都是基本資料類型,直接在棧上配置設定就可以了。是以,在對象不逃逸出作用域并且能夠分解為純标量表示時,對象就可以在棧上配置設定。

JVM提供了參數-XX:+EliminateAllocations來開啟标量替換,預設仍然是開啟的。顯然,如果把它關掉的話,就相當于禁止了棧上記憶體配置設定,隻有逃逸分析是無法發揮作用的。

在Debug版JVM中,還可以通過參數-XX:+PrintEliminateAllocations來檢視标量替換的具體情況。

除了标量替換之外,通過逃逸分析還能實作同步消除

(synchronization elision),當然它與本文的主題無關了。

舉個例子:

private void someMethod() {
    Object lockObject = new Object();
    synchronized (lockObject) {
      System.out.println(lockObject.hashCode());
    }
}      

lockObject這個鎖對象的生命期隻在someMethod()方法中,并不存在多線程通路的問題,是以synchronized塊并無意義,會被優化掉:

private void someMethod() {
    Object lockObject = new Object();
    System.out.println(lockObject.hashCode());
}