作者:LittleMagic
https://www.jianshu.com/p/8377e09971b8

為了防止歧義,可以換個說法:
Java物件實體和陣列元素都是在堆上分配記憶體的嗎?
答:不一定,滿足特定條件時,它們可以在(虛擬機)堆疊上分配記憶體,

JVM記憶體結構很重要,多多復習
這和我們平時的理解可能有些不同,虛擬機堆疊一般是用來存盤基本資料型別、參考和回傳地址的,怎么可以存盤實體資料了呢?
這是因為Java JIT(just-in-time)編譯器進行的兩項優化,分別稱作逃逸分析(escape analysis)和標量替換(scalar replacement),

注意看一下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());
}
推薦去我的博客閱讀更多:
1.Java JVM、集合、多執行緒、新特性系列教程
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
4.Java、后端、架構、阿里巴巴等大廠最新面試題
覺得不錯,別忘了點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/178809.html
標籤:Java
