1.前言
以編譯程序執(zhí)行本地代碼,比解釋執(zhí)行更快岖妄,除虛擬機(jī)解釋執(zhí)行字節(jié)碼額外消耗時(shí)間的原因之外,另一個(gè)很重要原因就是虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)幾乎把對代碼的所有優(yōu)化措施都集中在即時(shí)編譯器中藐俺。一般即時(shí)編譯器生成的本地代碼比javac產(chǎn)生的字節(jié)碼更優(yōu)秀兑燥。
HotSpot虛擬機(jī)在即時(shí)編譯器中采用的優(yōu)化技術(shù):https://wiki.openjdk.java.net/display/HotSpot/PerformanceTacticIndex
下面談一下jit編譯優(yōu)化的常見手段。
2 方法內(nèi)聯(lián)
編譯器最總要的優(yōu)化是方法內(nèi)聯(lián)琴锭。遵循面向?qū)ο蠓绞骄帉懙拇a晰甚,通常會有很多get,set方法决帖。
優(yōu)化前代碼:
static class B{
int value;
final int get(){
return value;
}
}
public void foo(){
y = b.get();
z = b.get();
sum = y + z;
}
內(nèi)聯(lián)是最重要的優(yōu)化措施厕九,原因有二。一地回,降低調(diào)用的開銷(如建立棧幀等)扁远;二,方法內(nèi)聯(lián)為其他優(yōu)化措施建立良好基礎(chǔ)刻像,方法內(nèi)聯(lián)膨脹后畅买,可在更大范圍采用后續(xù)優(yōu)化手段。
內(nèi)聯(lián)后代碼:
public void foo(){
y = b.value;
z = b.value;
sum = y + z;
}
第二步優(yōu)化:冗余訪問消除
y和z的表達(dá)式一樣细睡,所以可以不必去訪問對象b谷羞,替換為z=y。如果把b.value視為表達(dá)式溜徙,則該優(yōu)化也可以理解為公共表達(dá)式消除湃缎。
冗余存儲消除后代碼:
public void foo(){
y = b.value;
z = y;
sum = y + z;
}
第三步優(yōu)化:復(fù)寫傳播
z與y的值一樣,不必使用額外的變量蠢壹,用y代替z
復(fù)寫傳播優(yōu)化后:
public void foo(){
y = b.value;
y = y;
sum = y + y;
}
第四步優(yōu)化:無用代碼消除
public void foo(){
y = b.value;
sum = y + y;
}
經(jīng)過優(yōu)化以后嗓违,作用一樣,但是省略了很多代碼(從字節(jié)碼知残,機(jī)器碼指令上的差距更大)靠瞎,執(zhí)行效率更高比庄。
由上面的例子,我們可以看到乏盐,方法內(nèi)聯(lián)是很多優(yōu)化手段的基礎(chǔ)佳窑。方法是否內(nèi)聯(lián),取決于方法是否夠熱(jvm根據(jù)內(nèi)部方法判斷父能,如是否調(diào)用頻繁)神凑,以及大小。只有方法夠熱何吝,并且字節(jié)碼小于325字節(jié)溉委,或者方法小于35字節(jié)時(shí)才能內(nèi)聯(lián)。
由此我們可以和平時(shí)擼代碼的一個(gè)小經(jīng)驗(yàn)印證起來爱榕,多寫小方法瓣喊,把職責(zé)完整的邏輯段抽取出來,成為獨(dú)立的小方法黔酥。小方法好處有幾個(gè):
一 邏輯簡單藻三,職責(zé)單一,與設(shè)計(jì)原則的單一性對應(yīng)跪者;
二 抽出去小方法棵帽,類似的,職責(zé)上接近的渣玲,可以當(dāng)成下一步抽象優(yōu)化的基礎(chǔ)逗概;
三 小方法更容易滿足內(nèi)聯(lián)條件。
3 逃逸分析
逃逸分析與方法內(nèi)聯(lián)相似忘衍,也是其他優(yōu)化手段的基礎(chǔ)逾苫。
逃逸分析的行為就是分心對象動(dòng)態(tài)作用域,當(dāng)一個(gè)對象不會逃逸到方法或者線程之外淑履,也就是其他方法或者線程無法通過任何路徑訪問到該對象時(shí)隶垮,則可進(jìn)行一些高效優(yōu)化。
棧上分配:堆中分配的對象秘噪,無論是驗(yàn)證,整理或者清理都需耗費(fèi)時(shí)間勉耀,如果確定對象不會逃逸指煎,則可在棧上分配內(nèi)存,對象所在的內(nèi)存就可以隨棧幀出棧而銷毀便斥。在一般應(yīng)用中至壤,不會逃逸的局部對象比例很大,棧上分配可減少垃圾回收壓力枢纠。
同步消除:線程同步耗時(shí)很大像街,不會逃逸的對象,讀寫不會有競爭,相應(yīng)的同步措施可以消除镰绎。
標(biāo)量替換:標(biāo)量是指無法再分解的數(shù)據(jù)脓斩。java虛擬機(jī)中的原始數(shù)據(jù)類型都可以稱為標(biāo)量。如果可以繼續(xù)分解畴栖,則稱為聚合量随静,如對象。如果把一個(gè)java對象拆散吗讶,根據(jù)程序訪問的情況燎猛,將其實(shí)用到的成員變量恢復(fù)原始類型,來訪問照皆,叫做標(biāo)量替換重绷。如果逃逸分析證明一個(gè)對象不會被外部訪問,并且可以拆解膜毁,那么程序真正執(zhí)行的時(shí)候?qū)⒑芸赡懿粍?chuàng)建這個(gè)對象昭卓,改為直接創(chuàng)建該對象的成員變量來代替。進(jìn)而直接在棧上分配和讀寫爽茴,為后續(xù)優(yōu)化打基礎(chǔ)葬凳。
棧上存儲的數(shù)據(jù),很大概率會被虛擬機(jī)分配到物理機(jī)的高速寄存器中室奏。
可以看到火焰,逃逸分析也是jit優(yōu)化代碼的重要措施,那擼代碼時(shí)有啥常識可以印證胧沫?
答案:廣義的迪米特法則昌简。控制對象之間的信息流量绒怨,流向纯赎,影響。內(nèi)部實(shí)現(xiàn)和外部接口隔離南蹂。嚴(yán)格控制對象的作用域犬金,訪問權(quán)限等,使對象六剥,方法的影響最小晚顷,不但優(yōu)雅,而且更合編譯器口味疗疟。(全局變量自然不滿足逃逸分析)
4 再談對象作用域
前面都在聊jit该默,我們回到編譯方式執(zhí)行程序的場景(代碼不夠熱,解釋器執(zhí)行)策彤∷ㄐ洌看看下面這段代碼匣摘。
public static void main (String[] args)(){
{
byte[] a = new byte[64 * 1024 * 1024];
}
System.gc();
}
jvm 加上運(yùn)行參數(shù)"-verbose:gc",查看運(yùn)行日志
[GC 66846K->65888K(125632K), 0.0009397 secs]
[Full GC 65888K->65746K(125632K), 0.0051574 secs]
內(nèi)存居然沒被回收裹刮。改一下代碼
public static void main (String[] args)(){
{
byte[] a = new byte[64 * 1024 * 1024];
}
int b = 0;
System.gc();
}
再執(zhí)行一次音榜,回收了。必指。囊咏。
[GC 66401K->65778K(125632K), 0.0035471 secs]
[Full GC 65778K->218K(125632K), 0.0140596 secs]
原因在于局部變量表。局部變量表是一組變量值存儲空間塔橡,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量梅割。在第一段代碼中,雖然變量離開了作用域葛家,但是在此之后户辞,沒有其他局部變量表的讀寫,GC Roots一部分的局部變量表仍然保存著對它的關(guān)聯(lián)癞谒。正常情況下沒影響底燎,但如果遇到一些特殊情況(對象占用內(nèi)存大,此方法的棧幀長時(shí)間不能被回收弹砚,方法調(diào)用次數(shù)達(dá)不到j(luò)it編譯條件)双仍,則會采用手動(dòng)set為null的方式(用來代替那句b = 0,把局部變量表清空)桌吃。
然而有一個(gè)問題朱沃,解釋執(zhí)行和jit編譯后執(zhí)行是完全不同的代碼,這里用來救命的 set null茅诱,在jit編譯后逗物,很大可能會當(dāng)成無用代碼消除,jit編譯后瑟俭,第一段代碼可以正常釋放內(nèi)存翎卓。更優(yōu)雅的方式應(yīng)當(dāng)還是嚴(yán)格控制對象作用域,減少對set null的依賴摆寄。
5 總結(jié)
寫小方法
遵循廣義迪米特法則
嚴(yán)格控制對象作用域
6 參考
深入理解java虛擬機(jī)
java性能權(quán)威指南