一 什么是逃逸
逃逸是指在某個方法之內創建的對象,除了在方法體之內被引用之外,還在方法體之外被其它變量引用到;這樣帶來的后果是在該方法執行完畢之后,該方法中創建的對象將無法被GC回收,由於其被其它變量引用。
正常的方法調用中,方法體中創建的對象將在執行完畢之后,垃圾回收器將回收其中創建的對象;故由於無法回收,即成為逃逸。
逃逸分析的基本行為就是分析對象動態作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,稱為方法逃逸。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸。
方法逃逸的幾種方式如下:
public class EscapeTest { public static Object obj; public void globalVariableEscape() { // 給全局變量賦值,發生逃逸 obj = new Object(); } public static StringBuffer craeteStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; //方法返回值,發生逃逸 } public void instanceEscape() { // 實例引用發生逃逸 test(this); } }
如果開啟逃逸分析,那么即時編譯器(Just-in-time Compilation,JIT)就可以對代碼做如下優化:
(1)同步省略(鎖消除):如果確定一個對象不會逃逸出線程,即對象被發現只能被一個線程訪問到,無法被其它線程訪問到,那該對象的讀寫就不會存在競爭,對這個變量的同步措施就可以消除掉。
(2)將堆分配轉化為棧分配:棧上分配就是把方法中的變量和對象分配到棧上,方法執行完后棧自動銷毀,而不需要垃圾回收的介入,從而提高系統性能。。
(3)分離對象或標量替換。Java虛擬機中的原始數據類型(int,long等數值類型以及reference類型等)都不能再進一步分解,它們就可以稱為標量。相對的,如果一個數據可以繼續分解,那它稱為聚合量,Java中最典型的聚合量是對象。如果逃逸分析證明一個對象不會被外部訪問,並且這個對象是可分解的,那程序真正執行的時候將可能不創建這個對象,而改為直接創建它的若干個被這個方法使用到的成員變量來代替。拆散后的變量便可以被單獨分析與優化, 可以各自分別在棧幀或寄存器上分配空間,原本的對象就無需整體分配空間了。
在Java代碼運行時,通過JVM參數可指定是否開啟逃逸分析,
-XX:+DoEscapeAnalysis : 表示開啟逃逸分析
-XX:-DoEscapeAnalysis : 表示關閉逃逸分析 從jdk 1.7開始已經默認開始逃逸分析,如需關閉,需要指定-XX:-DoEscapeAnalysis
二 同步省略(鎖消除)
在動態編譯同步塊的時候,即時編譯器(Just-in-time Compilation,JIT)可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發布到其他線程。如果同步塊所使用的鎖對象通過這種分析被證實只能夠被一個線程訪問,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這個取消同步的過程就叫同步省略,也叫鎖消除。
如以下代碼:
public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } }
代碼中對hollis這個對象進行加鎖,但是hollis對象的生命周期只在f()方法中,每個線程進入到方法f()時,都會創建一個hollis對象,並不會被其他線程所訪問到,所以在JIT編譯階段就會被優化掉。優化成:
public void f() { Object hollis = new Object(); System.out.println(hollis); }
所以,在使用synchronized的時候,如果JIT經過逃逸分析之后發現並無線程安全問題的話,就會做鎖消除。
-XX:+EliminateLocks開啟鎖消除(jdk1.8默認開啟,其它版本未測試) -XX:-EliminateLocks 關閉鎖消除 鎖消除基於分析逃逸基礎之上,開啟鎖消除必須開啟逃逸分析
三 標量替換
標量(Scalar)是指一個無法再分解成更小的數據的數據。Java中的原始數據類型就是標量。相對的,那些還可以分解的數據叫做聚合量(Aggregate),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); }
可以看到,Point這個聚合量經過逃逸分析后,發現他並沒有逃逸,就被替換成兩個聚合量了。那么標量替換有什么好處呢?就是可以大大減少堆內存的占用。因為一旦不需要創建對象了,那么就不再需要分配堆內存了。
標量替換為棧上分配提供了很好的基礎。
四 棧上分配
在Java虛擬機中,對象是在Java堆中分配內存的,這是一個普遍的常識。但是,有一種特殊情況,那就是如果經過逃逸分析后發現,一個對象並沒有逃逸出方法的話,那么就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。
public class OnStackTest { public static void alloc(){ byte[] b=new byte[2]; b[0]=1; } public static void main(String[] args) { long b=System.currentTimeMillis(); for(int i=0;i<100000000;i++){ alloc(); } long e=System.currentTimeMillis(); System.out.println(e-b); } }
開啟逃逸分析,執行的時間為4毫秒。如下圖:

關閉逃逸分析,執行的時間為618毫秒,並且伴隨的大量的GC日志信息。如下圖:

開啟逃逸分析,對象沒有分配在堆上,沒有進行GC,而是把對象分配在棧上。
關閉逃逸分析,對象全部分配在堆上,當堆中對象存滿后,進行多次GC,導致執行時間大大延長。堆上分配比棧上分配慢上百倍。
參考:
1、深入分析JVM逃逸分析對性能的影響 https://blog.csdn.net/w372426096/article/details/80938788
2、深入分析JVM逃逸分析對性能的影響 https://blog.csdn.net/jijianshuai/article/details/73740024
