逃逸分析

逃逸分析

在編程語(yǔ)言的編譯優(yōu)化原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析。通俗來(lái)講,當(dāng)一個(gè)對(duì)象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了逃逸。

常見(jiàn)的逃逸場(chǎng)景:全局變量賦值、方法返回值、實(shí)例引用傳遞

public class A {

    public static B b;
    
    //給全局變量賦值,發(fā)生逃逸
    public void globalVariablePointerEscape(){
        b = new B();
    }

    //方法返回值,發(fā)生逃逸
    public B methodPointerEscape(){
        return new B();
    }

    //實(shí)例引用傳遞,發(fā)生逃逸
    public void instancePassPointerEscape(){
        methodPointerEscape().printClassName(this);
    }

}

public class B {

    public void printClassName(A a){
        System.out.println(a.getClass().getName());
    }

}

逃逸分析原理

我們知道Java對(duì)象是在堆里分配的,在調(diào)用棧中,只保存了對(duì)象的指針。當(dāng)對(duì)象不再使用后,需要依靠GC來(lái)遍歷引用樹(shù)并回收內(nèi)存,如果對(duì)象數(shù)量較多,將給GC帶來(lái)較大壓力。因此,減少臨時(shí)對(duì)象在堆內(nèi)存分配的數(shù)量是最有效的優(yōu)化方法。

場(chǎng)景應(yīng)用一:棧上分配

其實(shí),在java應(yīng)用里普遍存在一種場(chǎng)景。一般是在方法體內(nèi),聲明了一個(gè)局部變量,且該變量在方法執(zhí)行生命周期內(nèi)未發(fā)生逃逸(在方法體內(nèi),未將引用暴露給外面)。按照J(rèn)VM內(nèi)存分配機(jī)制,首先會(huì)在堆里創(chuàng)建變量類的實(shí)例,然后將返回的對(duì)象指針壓入調(diào)用棧,繼續(xù)執(zhí)行。這是優(yōu)化前,JVM的處理方式。

  • 逃逸分析優(yōu)化 - 棧上分配

分析找到未逃逸的變量,將變量類的實(shí)例化內(nèi)存直接在棧里分配(無(wú)需進(jìn)入堆),分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,最后線程結(jié)束,棧空間被回收,局部變量對(duì)象也被回收。對(duì)比可以看出,主要區(qū)別在??臻g直接作為臨時(shí)對(duì)象的存儲(chǔ)介質(zhì)。從而減少了臨時(shí)對(duì)象在堆內(nèi)的分配數(shù)量。

應(yīng)用場(chǎng)景二:同步消除

在即使編譯器時(shí),如果發(fā)現(xiàn)不可能被共享的對(duì)象,則可以消除這些對(duì)象的鎖操作。

也許你會(huì)覺(jué)得奇怪,既然有些對(duì)象不可能被多線程訪問(wèn),那為什么要加鎖呢?寫(xiě)代碼時(shí)直接不加鎖不就好了。但是有時(shí),這些鎖并不是程序員所寫(xiě)的,有的是JDK實(shí)現(xiàn)中就有鎖的,比如Vector和StringBuffer這樣的類,它們中的很多方法都是有鎖的。當(dāng)我們?cè)谝恍┎粫?huì)有線程安全的情況下使用這些類的方法時(shí),達(dá)到某些條件時(shí),編譯器會(huì)將鎖消除來(lái)提高性能。

public class BufferTest {

    public static void main(String[] args){
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i ++){
            createStringBuffer("JVM", "EscapeAnalysis");
        }
        long end = System.currentTimeMillis();
        System.out.println("it takes " + (end - start) + " ms");

    }

    public static String createStringBuffer(String s1, String s2){
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }

}

優(yōu)化前:
it takes 867 ms
優(yōu)化后:
it takes 802 ms

基于逃逸分析,JVM可以判斷,如果這個(gè)局部變量StringBuffer并沒(méi)有逃出它的作用域,那么可以確定這個(gè)StringBuffer并不會(huì)被多線程所訪問(wèn),那么就可以把這些多余的鎖給去掉來(lái)提高性能。

應(yīng)用場(chǎng)景三:標(biāo)量替換

Java虛擬機(jī)中的原始數(shù)據(jù)類型(int,long等數(shù)值類型以及reference類型等)都不能再進(jìn)一步分解,它們就可以稱為標(biāo)量。相對(duì)的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它稱為聚合量,Java中最典型的聚合量是對(duì)象。如果逃逸分析證明一個(gè)對(duì)象不會(huì)被外部訪問(wèn),并且這個(gè)對(duì)象是可分解的,那程序真正執(zhí)行的時(shí)候?qū)⒖赡懿粍?chuàng)建這個(gè)對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來(lái)代替。拆散后的變量便可以被單獨(dú)分析與優(yōu)化,可以各自分別在棧幀或寄存器上分配空間,原本的對(duì)象就無(wú)需整體分配空間了。

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

相關(guān)閱讀更多精彩內(nèi)容

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