逃逸分析在 Java 中的應(yīng)用與優(yōu)化
逃逸分析技術(shù)算是在JVM面試題偶有提及的一個考察點,當(dāng)然如果你能夠講解JVM工作原理的時候提及這一點,這一定會增加面試官對你的好感,通過對本篇文章的閱讀,你將能夠從容的解決以下幾個面試題:
- 什么是逃逸分析技術(shù)?
- 逃逸分析技術(shù)解決什么問題?帶來什么好處?
- 如何更好的理解或者運用逃逸分析技術(shù)?
什么是逃逸分析
逃逸分析技術(shù)是JVM用于提高性能以及節(jié)省內(nèi)存的手段,在JVM編譯語境下也就是我們常說的JIT階段,關(guān)于逃逸分析的概念,引用《深入理解Java虛擬機(jī)》的說法:
逃逸分析的基本原理是:分析對象動態(tài)作用域,當(dāng)一個對象在方法里面被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他方法中,這種稱為方法逃逸;甚至還有可能被外部線程訪問到,譬如賦值給可以在其他線程中訪問的實例變量,這種稱為線程逃逸;從不逃逸、方法逃逸到線程逃逸,稱為對象由低到高的不同逃逸程度。
只要編譯階段判定當(dāng)前對象并沒有發(fā)生逃逸,那么它就會采用棧上分配、標(biāo)量替換、同步鎖消除等手段提升程序執(zhí)行性能和節(jié)省內(nèi)存開銷,具體場景還得查看是發(fā)生方法逃逸還是線程逃逸。
那么我們又該如何判斷對象是否逃逸呢?我們不妨基于上述的判斷條件來看看這個示例,假設(shè)我們現(xiàn)在有一個user類,我們通過UserService進(jì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)然是肯定的,因為這段代碼方法內(nèi)所創(chuàng)建的對象被外部的main函數(shù)所引用,也就是我們所說的方法逃逸。
再來看看這段代碼,典型的在方法內(nèi)創(chuàng)建然后被外部函數(shù)所引用,也就是所謂的方法逃逸:
public User createUser() {
User user = new User();
user.setId(RandomUtil.randomInt(10));
user.setName(RandomUtil.randomString(3));
return user;
}
而這段stringBuffer 已經(jīng)被其他線程實例所訪問到,也就是典型的線程逃逸:
public static void main(String[] args) throws InterruptedException {
StringBuffer stringBuffer = new StringBuffer();
CountDownLatch countDownLatch = new CountDownLatch(3);
//調(diào)用appendStr操作stringBuffer
new Thread(() -> {
appendStr(stringBuffer);
countDownLatch.countDown();
}).start();
//循環(huán)拼接操作stringBuffer
new Thread(() -> {
for (int i = 0; i < 10; i++) {
stringBuffer.append("aaa");
}
countDownLatch.countDown();
}).start();
//循環(huán)拼接操作stringBuffer
new Thread(() -> {
for (int i = 0; i < 10; i++) {
stringBuffer.append("aaa");
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
System.out.println(stringBuffer);
}
如何運用到逃逸分析技術(shù)
1.棧上分配(針對未逃逸或方法逃逸)
下面這段代碼僅在方法內(nèi)部完成對象創(chuàng)建或者打印,其對象并沒有被外部方法所引用和暴露,對象就沒有發(fā)生逃逸,對于沒有發(fā)生逃逸的代碼或者上文中方法逃逸的代碼端,JIT會通過棧上分配減少內(nèi)存占用和GC壓力。
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.分離對象或標(biāo)量替換(針對未逃逸)
如果僅僅是操作未逃逸對象的某些簡單運算,我們同樣可以只在棧幀內(nèi)使用這個對象,如此JVM就會將這個對象打散,將對象打散為無數(shù)個小的局部變量,實現(xiàn)標(biāo)量替換。
如下所示,這段代碼沒有發(fā)生任何逃逸,JVM會避免創(chuàng)建Point ,而是通過棧上創(chuàng)建基本變量完成邏輯操作:
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.同步鎖消除(針對未逃逸線程)
這一點就比較有趣了,我們都知道使用StringBuffer可以保證線程安全,因為其操作函數(shù)都有帶synchronized關(guān)鍵字,那么請問這段代碼會上鎖嗎?
public void appendStr(int count) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < count; i++) {
sb.append("no: " + i + " ");
}
}
答案是不會,因為我們當(dāng)前操作的StringBuffer 對象并沒有發(fā)生線程逃逸,它僅僅在函數(shù)內(nèi)部進(jìn)行字符串操作,所以針對appendStr內(nèi)部邏輯,其直接將其優(yōu)化為StringBuilder:
4.線程逃逸分析的更進(jìn)一步
請問實例方法調(diào)用靜態(tài)方法,StringBuffer作為變量傳入,是否發(fā)生逃逸,直接創(chuàng)建一個main方法調(diào)用這段代碼,方法是否發(fā)生逃逸?
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ā)生了方法逃逸,但是沒有發(fā)生線程逃逸,但我們的代碼是單線程執(zhí)行這段代碼,即使StringBuffer 由外部傳入,函數(shù)內(nèi)部依然可以進(jìn)行鎖消除將其內(nèi)部的拼接邏輯用StringBuilder進(jì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();
}
答案是沒有發(fā)生線程逃逸,返回的字符串還是沒有被外部線程操作,所以最終還是被轉(zhuǎn)為StringBuilder:
而下面這段代碼就是典型的逃逸,可以看到多線程場景下StringBuffer 被多線程共享和訪問,此時JIT優(yōu)化就會視為對象逃逸:
public static void main(String[] args) throws InterruptedException {
StringBuffer stringBuffer = new StringBuffer();
CountDownLatch countDownLatch = new CountDownLatch(3);
//調(diào)用appendStr操作stringBuffer
new Thread(() -> {
appendStr(stringBuffer);
countDownLatch.countDown();
}).start();
//循環(huán)拼接操作stringBuffer
new Thread(() -> {
for (int i = 0; i < 10; i++) {
stringBuffer.append("aaa");
}
countDownLatch.countDown();
}).start();
//循環(huán)拼接操作stringBuffer
new Thread(() -> {
for (int i = 0; i < 10; i++) {
stringBuffer.append("aaa");
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
System.out.println(stringBuffer);
}
public static void appendStr(StringBuffer stringBuffer) {
for (int i = 0; i < 10; i++) {
stringBuffer.append(i);
}
}
所以appendStr在判定線程逃逸之后,并沒有將StringBuffer變?yōu)镾tringBuilder:
小結(jié)
合理的在棧幀上解決問題可以避免對象逃逸,從而讓JIT盡可能的去進(jìn)行優(yōu)化,這一點我想應(yīng)該是一個Java程序員對于代碼的極致追求了。