深入淺出逃逸分析:提升程序性能的利器
逃逸分析技術(shù)算是在JVM面試題偶有提及的一個(gè)考察點(diǎn),當(dāng)然如果你能夠講解JVM工作原理的時(shí)候提及這一點(diǎn),這一定會(huì)增加面試官對(duì)你的好感,本文主題內(nèi)容如下:
- 什么是逃逸分析技術(shù)?
 - 逃逸分析技術(shù)解決什么問(wèn)題?帶來(lái)什么好處?
 - 如何更好的理解或者運(yùn)用逃逸分析技術(shù)?
 

一、什么是逃逸分析
逃逸分析技術(shù)是JVM用于提高性能以及節(jié)省內(nèi)存的手段,在JVM編譯語(yǔ)境下也就是我們常說(shuō)的JIT階段,逃逸分析技術(shù)通過(guò)以下兩個(gè)條件判斷該對(duì)象是否是逃逸:
- 該對(duì)象是否分配在堆上(static關(guān)鍵字或者成員變量)。
 - 該對(duì)象是否會(huì)傳給未知代碼,比如return到外部給別的類使用。
 
只要編譯階段判定當(dāng)前對(duì)象并沒(méi)有發(fā)生逃逸,那么它就會(huì)采用棧上分配、標(biāo)量替換、同步鎖消除等手段提升程序執(zhí)行性能和節(jié)省內(nèi)存開(kāi)銷。
那么我們又該如何判斷對(duì)象是否逃逸呢?我們不妨基于上述的判斷條件來(lái)看看這個(gè)示例,假設(shè)我們現(xiàn)在有一個(gè)user類:
@Data
public class User {
    private int id;
    private String name;
}我們通過(guò)UserService進(jìn)行初始化,那么請(qǐng)問(wèn)這段代碼是否發(fā)生逃逸呢?
public class UserService {
    private User user;
    public void init() {
        user = new User();
        user.setId(RandomUtil.randomInt(10));
        user.setName(RandomUtil.randomString(3));
    }
}答案當(dāng)然是肯定的,因?yàn)檫@段代碼會(huì)被外部的其他任意線程操作。
再來(lái)看看這段代碼,典型的return語(yǔ)句,很明顯的外部線程可以直接操作這個(gè)對(duì)象,所以這個(gè)對(duì)象也發(fā)生了逃逸,所以針對(duì)這幾種情況JIT都無(wú)法對(duì)其進(jìn)行優(yōu)化。
public User createUser() {
        User user = new User();
        user.setId(RandomUtil.randomInt(10));
        user.setName(RandomUtil.randomString(3));
        return user;
    }二、如何運(yùn)用到逃逸分析技術(shù)
1.棧上分配
一般來(lái)說(shuō),JIT即時(shí)編譯技術(shù)中的棧上分配和標(biāo)量替換基本都是同時(shí)出現(xiàn)的,按照上文所述,假如上述代碼所返回的user對(duì)象僅僅是獲取當(dāng)前用戶的年齡,那么我們就可以直接在方法內(nèi)完成邏輯計(jì)算并直接返回,這樣對(duì)象就沒(méi)有發(fā)生逃逸,如此對(duì)象便可直接在棧幀上進(jìn)行分配,有效減小JVM垃圾回收的壓力。
 Map<Integer, User> userMap = new HashMap<>();
    public int getUserAgeById(int id) {
       User user = new User();
        user.setId(RandomUtil.randomInt(10));
        user.setName(RandomUtil.randomString(3));
        //打印用戶信息
        printUserInfo(user);
    }2.分離對(duì)象或標(biāo)量替換
如果僅僅是操作未逃逸對(duì)象的某些簡(jiǎn)單運(yùn)算,我們同樣可以只在棧幀內(nèi)使用這個(gè)對(duì)象,如此JVM就會(huì)將這個(gè)對(duì)象打散,將對(duì)象打散為無(wú)數(shù)個(gè)小的局部變量,實(shí)現(xiàn)標(biāo)量替換,如下所示,這段代碼沒(méi)有發(fā)生逃逸,則JVM會(huì)避免創(chuàng)建Point 。
public static void main(String args[]) {
    alloc();
}
class Point {
    private int x;
    private int y;
}
private static void alloc() {
    Point point = new Point(1,2);
    System.out.println("point.x" + point.x + ";point.y" + point.y);
}進(jìn)而直接標(biāo)量替換,直接在棧上分配x和y的值,完成輸出打印。
private static void alloc() {
    int x = 1;
    int y = 2;
    System.out.println("point.x = " + x + "; point.y=" + y);
}3.同步鎖消除
這一點(diǎn)就比較有趣了,我們都知道使用StringBuffer可以保證線程安全,因?yàn)槠洳僮骱瘮?shù)都有帶synchronized關(guān)鍵字,那么請(qǐng)問(wèn)這段代碼會(huì)上鎖嗎?
public void appendStr(int count) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < count; i++) {
            sb.append("no: " + i + " ");
        }
    }答案是不會(huì),因?yàn)槲覀儺?dāng)前操作的StringBuffer 對(duì)象并沒(méi)有發(fā)生逃逸,它僅僅是根據(jù)外部傳入的count完成拼接并打印結(jié)果而已,于是JIT就會(huì)進(jìn)行鎖消除的優(yōu)化操作。如下字節(jié)碼所示,優(yōu)化后的StringBuffer被替換為StringBuilder。

三、逃逸分析更進(jìn)一步
了解了逃逸分析止之后,我們不妨基于下面這些題目進(jìn)行一下自測(cè),如下代碼,請(qǐng)問(wèn)實(shí)例方法調(diào)用靜態(tài)方法,StringBuffer作為變量傳入,是否發(fā)生逃逸,最終執(zhí)行代碼是StringBuffer 還是StringBuilder?
public void appendStr(int count) {
        StringBuffer sb = new StringBuffer();
        loop(count, sb);
    }
    private static void loop(int count, StringBuffer sb) {
        for (int i = 0; i < count; i++) {
            sb.append("no: " + i + " ");
        }
    }答案是未發(fā)生逃逸,因?yàn)閷?duì)象并沒(méi)有被外部線程操作,JIT感知到未發(fā)生逃逸,所以將StringBuffer 轉(zhuǎn)為StringBuilder。

再來(lái)看看這段代碼,請(qǐng)問(wèn)發(fā)生逃逸了嗎?
 public void appendStr(int count) {
        StringBuffer sb = new StringBuffer();
        loop(count, sb);
    }
    private static String loop(int count, StringBuffer sb) {
        for (int i = 0; i < count; i++) {
            sb.append("no: " + i + " ");
        }
        return sb.toString();
    }答案還是沒(méi)有,返回的字符串還是沒(méi)有被外部線程操作,所以最終還是被轉(zhuǎn)為StringBuilder:

四、小結(jié)
合理的在棧幀上解決問(wèn)題可以避免對(duì)象逃逸,從而讓JIT盡可能的去進(jìn)行優(yōu)化,這一點(diǎn)我想應(yīng)該是一個(gè)Java程序員對(duì)于代碼的極致追求了。















 
 
 






 
 
 
 