JVM之逃逸分析
什么是逃逸分析
在編譯程序優(yōu)化理論中,逃逸分析是一種確定指針動態(tài)范圍的方法——分析在程序的哪些地方可以訪問到指針。它涉及到指針分析和形狀分析。
當(dāng)一個變量(或?qū)ο?在子程序中被分配時,一個指向變量的指針可能逃逸到其它執(zhí)行線程中,或是返回到調(diào)用者子程序。如果使用尾遞歸優(yōu)化(通常在函數(shù)編程語言中是需要的),對象也可以看作逃逸到被調(diào)用的子程序中。如果一種語言支持第一類型的延續(xù)性在Scheme和Standard ML of New Jersey中同樣如此),部分調(diào)用棧也可能發(fā)生逃逸。
如果一個子程序分配一個對象并返回一個該對象的指針,該對象可能在程序中被訪問到的地方無法確定——這樣指針就成功“逃逸”了。如果指針存儲在全局變量或者其它數(shù)據(jù)結(jié)構(gòu)中,因為全局變量是可以在當(dāng)前子程序之外訪問的,此時指針也發(fā)生了逃逸。
逃逸分析確定某個指針可以存儲的所有地方,以及確定能否保證指針的生命周期只在當(dāng)前進程或在其它線程中。
下面我們看看Java中的逃逸分析是怎樣的?
Java的逃逸分析只發(fā)在JIT的即時編譯中,為什么不在前期的靜態(tài)編譯中就進行呢,知乎上已經(jīng)有過這樣的提問。
簡單來說是可以的,但是Java的分離編譯和動態(tài)加載使得前期的靜態(tài)編譯的逃逸分析比較困難或收益較少,所以目前Java的逃逸分析只發(fā)在JIT的即時編譯中,因為收集到足夠的運行數(shù)據(jù)JVM可以更好的判斷對象是否發(fā)生了逃逸。關(guān)于JIT即時編譯可參考JVM系列之走進JIT。
JVM判斷新創(chuàng)建的對象是否逃逸的依據(jù)有:
一、對象被賦值給堆中對象的字段和類的靜態(tài)變量。
二、對象被傳進了不確定的代碼中去運行。
如果滿足了以上情況的任意一種,那這個對象JVM就會判定為逃逸。對于第一種情況,因為對象被放進堆中,則其它線程就可以對其進行訪問,所以對象的使用情況,編譯器就無法再進行追蹤。第二種情況相當(dāng)于JVM在解析普通的字節(jié)碼的時候,如果沒有發(fā)生JIT即時編譯,編譯器是不能事先完整知道這段代碼會對對象做什么操作。保守一點,這個時候也只能把對象是當(dāng)作是逃逸來處理。下面舉幾個例子
- public class EscapeTest {
- public static Object globalVariableObject;
- public Object instanceObject;
- public void globalVariableEscape(){
- globalVariableObject = new Object(); //靜態(tài)變量,外部線程可見,發(fā)生逃逸
- }
- public void instanceObjectEscape(){
- instanceObject = new Object(); //賦值給堆中實例字段,外部線程可見,發(fā)生逃逸
- }
- public Object returnObjectEscape(){
- return new Object(); //返回實例,外部線程可見,發(fā)生逃逸
- }
- public void noEscape(){
- synchronized (new Object()){
- //僅創(chuàng)建線程可見,對象無逃逸
- }
- Object noEscape = new Object(); //僅創(chuàng)建線程可見,對象無逃逸
- }
- }
基于逃逸分析的優(yōu)化
當(dāng)判斷出對象不發(fā)生逃逸時,編譯器可以使用逃逸分析的結(jié)果作一些代碼優(yōu)化
將堆分配轉(zhuǎn)化為棧分配。如果某個對象在子程序中被分配,并且指向該對象的指針永遠不會逃逸,該對象就可以在分配在棧上,而不是在堆上。在有垃圾收集的語言中,這種優(yōu)化可以降低垃圾收集器運行的頻率。
同步消除。如果發(fā)現(xiàn)某個對象只能從一個線程可訪問,那么在這個對象上的操作可以不需要同步。
分離對象或標(biāo)量替換。如果某個對象的訪問方式不要求該對象是一個連續(xù)的內(nèi)存結(jié)構(gòu),那么對象的部分(或全部)可以不存儲在內(nèi)存,而是存儲在CPU寄存器中。
對于優(yōu)化一將堆分配轉(zhuǎn)化為棧分配,這個優(yōu)化也很好理解。下面以代碼例子說明:
虛擬機配置參數(shù):-XX:+PrintGC -Xms5M -Xmn5M -XX:+DoEscapeAnalysis
- -XX:+DoEscapeAnalysis表示開啟逃逸分析,JDK8是默認開啟的
- -XX:+PrintGC 表示打印GC信息
- -Xms5M -Xmn5M 設(shè)置JVM內(nèi)存大小是5M
- public static void main(String[] args){
- for(int i = 0; i < 5_000_000; i++){
- createObject();
- }
- }
- public static void createObject(){
- new Object();
- }
運行結(jié)果是沒有GC。
把虛擬機參數(shù)改成 -XX:+PrintGC -Xms5M -Xmn5M -XX:-DoEscapeAnalysis。關(guān)閉逃逸分析得到結(jié)果的部分截圖是,說明了進行了GC,并且次數(shù)還不少。
- [GC (Allocation Failure) 4096K->504K(5632K), 0.0012864 secs]
- [GC (Allocation Failure) 4600K->456K(5632K), 0.0008329 secs]
- [GC (Allocation Failure) 4552K->424K(5632K), 0.0006392 secs]
- [GC (Allocation Failure) 4520K->440K(5632K), 0.0007061 secs]
- [GC (Allocation Failure) 4536K->456K(5632K), 0.0009787 secs]
- [GC (Allocation Failure) 4552K->440K(5632K), 0.0007206 secs]
- [GC (Allocation Failure) 4536K->520K(5632K), 0.0009295 secs]
- [GC (Allocation Failure) 4616K->512K(4608K), 0.0005874 secs]
這說明了JVM在逃逸分析之后,將對象分配在了方法createObject()方法棧上。方法棧上的對象在方法執(zhí)行完之后,棧楨彈出,對象就會自動回收。這樣的話就不需要等內(nèi)存滿時再觸發(fā)內(nèi)存回收。這樣的好處是程序內(nèi)存回收效率高,并且GC頻率也會減少,程序的性能就提高了。
優(yōu)化二 同步鎖消除
如果發(fā)現(xiàn)某個對象只能從一個線程可訪問,那么在這個對象上的操作可以不需要同步。
虛擬機配置參數(shù):-XX:+PrintGC -Xms500M -Xmn500M -XX:+DoEscapeAnalysis。配置500M是保證不觸發(fā)GC。
- public static void main(String[] args){
- long start = System.currentTimeMillis();
- for(int i = 0; i < 5_000_000; i++){
- createObject();
- }
- System.out.println("cost = " + (System.currentTimeMillis() - start) + "ms");
- }
- public static void createObject(){
- synchronized (new Object()){
- }
- }
運行結(jié)果
- cost = 6ms
把逃逸分析關(guān)掉:-XX:+PrintGC -Xms500M -Xmn500M -XX:-DoEscapeAnalysis
運行結(jié)果
- cost = 270ms
說明了逃逸分析把鎖消除了,并在性能上得到了很大的提升。這里說明一下Java的逃逸分析是方法級別的,因為JIT的即時編譯是方法級別。
優(yōu)點三 分離對象或標(biāo)量替換。
這個簡單來說就是把對象分解成一個個基本類型,并且內(nèi)存分配不再是分配在堆上,而是分配在棧上。這樣的好處有,一、減少內(nèi)存使用,因為不用生成對象頭。 二、程序內(nèi)存回收效率高,并且GC頻率也會減少,總的來說和上面優(yōu)點一的效果差不多。
OK,現(xiàn)在我們又知道了一件聰明的JVM在背后為我們做的事了。