提到JVM,相信大家一定知道JVM是什麼?但是,提到逃逸分析,相信大多數人都可能一臉懵逼,逃逸分析到底是什麼呢?接下來給大家分享一下。
在Java的編譯體系中,一個Java的原始碼檔案變成計算機可執行的機器指令的過程中,需要經歷兩段編譯,第一段編譯就是透過javac命令把java檔案編譯成JVM可以識別的class檔案,第二段編譯就是把class檔案翻譯成位元組碼指令,讓計算機識別。
在第二段編譯中,JVM透過解釋位元組碼將其翻譯成對應的機器指令,逐條讀入,逐條解釋翻譯。很顯然,經過解釋執行,其執行速度必然會比執行二進位制位元組碼程式慢很多。這就是傳統的JVM的直譯器的功能。為了解決這種效率問題,引入了JIT(即時編譯)技術。
引入JIT技術,Java程式雖然還是透過直譯器進行解釋執行,但是,當某個方法執行呼叫的次數比較多的時候,就會被JVM認為是個"熱點程式碼"。那麼,如果是你,你會想到怎麼做呢?誒,沒錯,把這些"熱點程式碼"快取起來。JIT也是這麼做的,JIT會把部分"熱點程式碼"翻譯成本地機器相關的機器碼,並進行最佳化,然後再把翻譯後的機器碼快取起來,以備下次使用。
JVM記憶體分配策略在JVM中,JVM管理的記憶體包括方法區,虛擬機器棧,本地方法棧,堆,程式計數器等。(這裡就不一一介紹了,感興趣的同學我之後會寫JVM的文章)
一般情況下,JVM執行時資料都儲存在棧和堆中。棧用來存放一些基本變數和物件的引用(當然這不是絕對的後面會介紹到),堆用來存放陣列的元素和物件,也就是new出來的具體例項。
隨著JIT編譯器的發展與逃逸分析的技術成熟。棧上分配,標量替換最佳化技術就會導致物件都分配到堆上這個說法變的不是那麼絕對了。
什麼是逃逸分析?逃逸分析就是,當一個物件被new出來之後,它可能被外部所呼叫,如果是作為引數傳遞到外部了,就稱之為方法逃逸。
例如:
非方法逃逸:
public static void returnStr(){ User user = new User(); user.setId(1); user.setName("張三"); user.setAge(18);}public static String returnStr(){ User user = new User(); user.setId(1); user.setName("張三"); user.setAge(18); return user.toString();//這裡User要實現get,set方法,還要實現toString方法}
方法逃逸:
public static User returnStr(){ User user = new User(); user.setId(1); user.setName("張三"); user.setAge(18); return user;//這裡User要實現get,set方法}
大家應該看出區別了吧,這裡第一段的兩個方法均沒有逃逸,而第二段的方法卻逃逸了,這說明,想要逃逸方法的話,需要讓物件本身被外部呼叫。
使用逃逸分析,編譯器可以對程式碼做以下最佳化:
同步省略。如果一個物件被發現只能從一個執行緒被訪問到,那麼對於這個物件的操作可以不考慮同步將堆分配轉換為棧分配。如果一個物件在子程式中被分配,要使指向該物件的指標永遠不會逃逸,物件可能是棧分配的候選,而不是堆分配。分離物件或標量替換,有點物件可能不需要作為一個連續的記憶體結構存在也可以被訪問到,那麼物件的部分(或全部)可以不儲存在記憶體,而是儲存在CPU暫存器中。這裡開啟和關閉逃逸分析用這個:
-XX:+DoEscapeAnalysis : 表示開啟逃逸分析
-XX:-DoEscapeAnalysis : 表示關閉逃逸分析 從jdk 1.7開始已經預設開始逃逸分析,如需關閉,需要指定-XX:-DoEscapeAnalysis
實戰:透過列印GC來觀察是否是棧上分配public class Test { public static void main(String[] args) { Test test = new Test(); test.createUser(); System.out.println("11"); } public void createUser(){ int i = 0; while(true){ User user = new User(); user.setId(i); user.setName("張三"); user.setAge(18); i++; } }}public class User { private int id; private String name; private int age; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; }}
這裡有兩個類,第一個是Test類,一個是User實體類,Test類中,死迴圈來進行建立User物件,在idea中配置 -XX:+PrintGC引數來負責列印GC資訊,啟動類
結果:
這裡可以看到程式一直沒有結束,但是GC資訊一直沒有列印,這時候我們把逃逸分析關閉 在idea中配置這兩個-XX:+PrintGC -XX:-DoEscapeAnalysis
再次啟動類
這裡可以看到控制檯一直在輸出GC資訊,這樣是不是也可以得出結論,開啟逃逸分析之後如果不是逃逸方法,那麼物件就是在棧上分配。
同步省略在動態編譯同步塊的時候,JIT編譯器可以藉助逃逸分析來判斷同步塊所使用的鎖物件是否只能夠被一個執行緒訪問而沒有釋出到其他執行緒。
如果同步塊所使用的鎖物件透過這種分析被證明只能被一個執行緒訪問,那麼JIT編譯器在編譯這個同步塊的時候就會取消這部分程式碼的同步,這個取消同步就叫做同步省略,也叫鎖消除。
標量替換透過逃逸分析確定該物件不會被外部訪問,並且物件可以被進一步分解時,JVM不會建立該物件,而是將該物件成員變數分解若干個被這個方法使用的成員變數所代替,這些代替的成員變數在棧幀或暫存器上分配空間,這樣就不會因為沒有一大塊連續空間導致物件記憶體不夠分配。開啟標量替換引數-XX:+EliminateAllocations,JDK7之後預設開啟
標量與聚合量標量即不可被進一步分解的量,而Java的基本資料型別就是標量(比如int,long等基本資料型別以及reference型別等),標量的對立就是可以被進一步分解的量,而這種量稱之為聚合量。而在Java中物件就是可以被進一步分解的聚合量
在JIT階段,如果經過逃逸分析,發現一個物件不被外界訪問的話,那麼經過JIT最佳化,就會把這個物件拆解成若干個其中包含若干個成員變數來替代。
public static void main(String[] args) { alloc();}private static void alloc() { Point point = new Point(1,2); System.out.println("point.x="+point.x+"; point.y="+point.y);}class Point{ private int x; private int y;}
以上程式碼中,point物件並沒有逃逸出alloc方法,並且point物件是可以拆解成標量的。那麼,JIT就會不會直接建立Point物件,而是直接使用兩個標量int x ,int y來替代Point物件。
替換後:
private static void alloc() { int x = 1; int y = 2; System.out.println("point.x="+x+"; point.y="+y);}
這種替換可以大大減少堆記憶體的佔用,因為一旦不需要建立物件了,那麼就不需要分配堆記憶體了。
連結:https://juejin.cn/post/6905629726573133837