一行代碼引發(fā)的性能暴跌 10 倍!
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!
編輯:業(yè)余草
來源:juejin.cn/post/7276999034962280508
推薦:https://t.zsxq.com/12FEd8lJL
自律才能自由
今天給大家分享一個(gè),最近遇到的性能優(yōu)化問題。線上的業(yè)務(wù)邏輯比較復(fù)雜,本文只提供相關(guān)代碼的簡化程序。
下面我們直接上測試代碼。
代碼測試
import com.google.common.base.Stopwatch;
import java.util.concurrent.TimeUnit;
public class StackTest {
public static void main(String[] args) {
Stopwatch started = new Stopwatch();
started.start();
User user = null;
for (long i = 0; i < 1000_000_000; i++) {
user = new User();
}
started.stop();
System.out.println(started.elapsed(TimeUnit.MILLISECONDS) + "ms");
//不加打印 300ms
//加了打印 3000ms
// System.out.println(user);
}
}
class User {
private int age;
private String userName;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
上面的一個(gè)簡單的代碼是測試 Java 創(chuàng)建對象的性能,如果沒有 System.out.println(user); 輸出的時(shí)間是 300ms左右,如果加上性能是 3000ms 左右,整整慢了 10 倍左右。(具體需要時(shí)間根據(jù)電腦的配置決定)。
有人可能會(huì)說,System.out.println函數(shù)有鎖(都 9102 了,你還不知道 System.out.println 的危害!),但要注意,這里是單線程,Java 會(huì)鎖消除。
看似很簡單的代碼,卻會(huì)帶來這樣的性能消耗,確實(shí)很讓人費(fèi)解。為了弄清楚這個(gè)問題,我們需要討論下,java 代碼分配的規(guī)則。
對象分配規(guī)則
在前面的博客已經(jīng)提過 Java 對象的分配過程,具體流程圖如下:
棧上分配
棧上分配是 Java 虛擬機(jī)提供的一項(xiàng)優(yōu)化技術(shù),將線程私有的對象打散分配在棧上,棧上分配的對象回收直接 POP 出站,不需要垃圾回收器的介入,效率很高。當(dāng)然棧上分配也需要一些特殊的條件:
-
棧空間小,對于大對象無法實(shí)現(xiàn)棧上分配 -
對象不能出現(xiàn)逃逸(JVM 參數(shù): -XX:+DoEscapeAnalysis) -
對象可以進(jìn)行標(biāo)量替換,即是使用字段來表示對象( -XX:+EliminateAllocations)。
如 demo 所示,我們可以是用 age 和 username 兩個(gè)字段來代替 User 對象。
TLAB 分配
TLAB Thread Local Allocation Buffer, 即:線程本地分配緩存。這是一塊線程專用的內(nèi)存分配區(qū)域。TLAB 占用的是 eden 區(qū)的空間。在TLAB 啟用的情況下(默認(rèn)開啟),JVM會(huì)為每一個(gè)線程分配一塊TLAB區(qū)域。
使用 TLAB 是為了加速對象的分配。由于對象一般分配在堆上,而堆是線程共用的,因此可能會(huì)有多個(gè)線程在堆上申請空間,而每一次的對象分配都必須線程同步,會(huì)使分配的效率下降。
考慮到對象分配幾乎是 Java中 最常用的操作,因此 JVM 使用了 TLAB 這樣的線程專有區(qū)域來避免多線程沖突,提高對象分配的效率。
同樣,TLAB 空間一般不會(huì)太大(占用 eden 區(qū)),所以大對象無法進(jìn)行 TLAB 分配,只能直接分配到堆上。
分配策略
一個(gè)100KB的TLAB區(qū)域,如果已經(jīng)使用了80KB,當(dāng)需要分配一個(gè)30KB的對象時(shí),TLAB是如何分配的呢?可以有兩種情況:
-
廢棄當(dāng)前的 TLAB,重新申請; -
將這個(gè) 30KB 的對象直接分配到堆上,保留當(dāng)前 TLAB(當(dāng)有小于 20KB 的對象請求 TLAB 分配時(shí)可以直接使用該 TLAB 區(qū)域)。
JVM選擇的策略是:在虛擬機(jī)內(nèi)部維護(hù)一個(gè)叫 refill_waste 的值,當(dāng)請求對象大于 refill_waste 時(shí),會(huì)選擇在堆中分配,反之,則會(huì)廢棄當(dāng)前 TLAB,新建 TLAB來分配新對象。【默認(rèn)情況下,TLAB和refill_waste都是會(huì)在運(yùn)行時(shí)不斷調(diào)整的,使系統(tǒng)的運(yùn)行狀態(tài)達(dá)到最優(yōu)。】
JVM參數(shù)解析
| 參數(shù) | 作用 | 備注 |
|---|---|---|
| -XX:+UseTLAB | 啟用TLAB | 默認(rèn)啟用 |
| -XX:TLABRefillWasteFraction | 設(shè)置允許空間浪費(fèi)的比例 | 默認(rèn)值:64,即:使用1/64的TLAB空間大小作為refill_waste值 |
| -XX:-ResizeTLAB | 禁止系統(tǒng)自動(dòng)調(diào)整TLAB大小 | |
| -XX:TLABSize | 指定TLAB大小 | 單位:B |
Demo 分析
通過上面的分析,可以剖析出原因了,在使用打印的時(shí)候?qū)е铝?user 對象的逃逸,所以導(dǎo)致在棧上分配條件不滿足,只能在堆上分配,這樣就會(huì)導(dǎo)致頻繁的 GC,效率低下。
如果我們再使用(-XX:+UseTLAB)關(guān)閉 TLAB分配原則,則會(huì)導(dǎo)致分配的速度又會(huì)降低一點(diǎn)(TLAB 一般會(huì)對多線程競爭分配的時(shí)候提升比較明顯,此處不再驗(yàn)證)
