Java對象都是在堆上分配內(nèi)存嗎?

為了防止歧義,可以換個說法:Java對象實例和數(shù)組元素都是在堆上分配內(nèi)存的嗎?
答:不一定。滿足特定條件時,它們可以在(虛擬機)棧上分配內(nèi)存。

image

JVM內(nèi)存結(jié)構(gòu)很重要,多多復(fù)習(xí)

這和我們平時的理解可能有些不同。虛擬機棧一般是用來存儲基本數(shù)據(jù)類型、引用和返回地址的,怎么可以存儲實例數(shù)據(jù)了呢?這是因為Java JIT(just-in-time)編譯器進(jìn)行的兩項優(yōu)化,分別稱作逃逸分析(escape analysis)標(biāo)量替換(scalar replacement)。JIT是個復(fù)雜的話題,本文不贅述,看官如果想進(jìn)一步了解的話,可以參考這篇文章,它里面提供了幾篇有用的參考資料。

image

注意看一下JIT的位置

中文維基上對逃逸分析的描述基本準(zhǔn)確,摘錄如下:

在編譯程序優(yōu)化理論中,逃逸分析是一種確定指針動態(tài)范圍的方法——分析在程序的哪些地方可以訪問到指針。當(dāng)一個變量(或?qū)ο螅┰谧映绦蛑斜环峙鋾r,一個指向變量的指針可能逃逸到其它執(zhí)行線程中,或是返回到調(diào)用者子程序。
如果一個子程序分配一個對象并返回一個該對象的指針,該對象可能在程序中被訪問到的地方無法確定——這樣指針就成功“逃逸”了。如果指針存儲在全局變量或者其它數(shù)據(jù)結(jié)構(gòu)中,因為全局變量是可以在當(dāng)前子程序之外訪問的,此時指針也發(fā)生了逃逸。
逃逸分析確定某個指針可以存儲的所有地方,以及確定能否保證指針的生命周期只在當(dāng)前進(jìn)程或線程中。

簡單來講,JVM中的逃逸分析可以通過分析對象引用的使用范圍(即動態(tài)作用域),來決定對象是否要在堆上分配內(nèi)存,也可以做一些其他方面的優(yōu)化。

以下的例子說明了一種對象逃逸的可能性。

public  static StringBuilder getStringBuilder1(String a, String b) { 
  StringBuilder builder = new StringBuilder(a); 
  builder.append(b);
  return builder;   // builder通過方法返回值逃逸到外部 
 }
 public   static String getStringBuilder2(String a, String b) {
  StringBuilder builder = new StringBuilder(a); 
  builder.append(b);
  return builder.toString();  // builder范圍維持在方法內(nèi)部,未逃逸  
}

以JDK 1.8為例,可以通過設(shè)置JVM參數(shù)-XX:+DoEscapeAnalysis、-XX:-DoEscapeAnalysis來開啟或關(guān)閉逃逸分析(默認(rèn)當(dāng)然是開啟的)。下面先寫一個沒有對象逃逸的例子。

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);  
}  
public  static void allocate() {
  MyObject myObject = new MyObject(2019, 2019.0);  
}   
public  static class MyObject {
  int a; 
  double b;
  MyObject(int a, double b) { 
     this.a = a;
     this.b = b; 
  }  
}}

然后通過開啟和關(guān)閉DoEscapeAnalysis開關(guān)觀察不同。

  • 關(guān)閉逃逸分析
~ java -XX:-DoEscapeAnalysis EscapeAnalysisTest76 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 EscapeAnalysisTest4 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# ......

可見,關(guān)閉逃逸分析之后,堆上有5000000個MyObject實例,而開啟逃逸分析之后,就只剩下90871個實例了,不管是實例數(shù)還是內(nèi)存占用都只有原來的2%不到。另外,如果把堆內(nèi)存限制得小一點(比如加上-Xms10m -Xmx10m),并且打印GC日志(-XX:+PrintGCDetails)的話,關(guān)閉逃逸分析還會造成頻繁的GC,開啟逃逸分析就沒有這種情況。這說明逃逸分析確實降低了堆內(nèi)存的壓力。

但是,逃逸分析只是棧上內(nèi)存分配的前提,接下來還需要進(jìn)行標(biāo)量替換才能真正實現(xiàn)。

所謂標(biāo)量,就是指JVM中無法再細(xì)分的數(shù)據(jù),比如int、long、reference等。相對地,能夠再細(xì)分的數(shù)據(jù)叫做聚合量。仍然考慮上面的例子,MyObject就是一個聚合量,因為它由兩個標(biāo)量a、b組成。通過逃逸分析,JVM會發(fā)現(xiàn)myObject沒有逃逸出allocate()方法的作用域,標(biāo)量替換過程就會將myObject直接拆解成a和b,也就是變成了:

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

可見,對象的分配完全被消滅了,而int、double都是基本數(shù)據(jù)類型,直接在棧上分配就可以了。所以,在對象不逃逸出作用域并且能夠分解為純標(biāo)量表示時,對象就可以在棧上分配。

JVM提供了參數(shù)-XX:+EliminateAllocations來開啟標(biāo)量替換,默認(rèn)仍然是開啟的。顯然,如果把它關(guān)掉的話,就相當(dāng)于禁止了棧上內(nèi)存分配,只有逃逸分析是無法發(fā)揮作用的。在Debug版JVM中,還可以通過參數(shù)-XX:+PrintEliminateAllocations來查看標(biāo)量替換的具體情況。

除了標(biāo)量替換之外,通過逃逸分析還能實現(xiàn)同步消除(synchronization elision),當(dāng)然它與本文的主題無關(guān)了。舉個例子:

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

lockObject這個鎖對象的生命期只在someMethod()方法中,并不存在多線程訪問的問題,所以synchronized塊并無意義,會被優(yōu)化掉:

private void someMethod() {
  Object lockObject = new Object();
  System.out.println(lockObject.hashCode());  
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容