運(yùn)行期優(yōu)化
樓主最近在網(wǎng)上看到一篇寫關(guān)于JVM運(yùn)行期優(yōu)化的博客杈女,經(jīng)過整理洋丐,現(xiàn)在分享給大家:
我們知道淑掌,Java 是解釋執(zhí)行的蒿讥,可是解釋執(zhí)行畢竟還是有點(diǎn)慢的,這也使得 Java 一直被認(rèn)為是效率低下的語言……,不過隨著即時(shí)編譯技術(shù)的發(fā)展芋绸,Java 的運(yùn)行速度得到了很大的提升媒殉,在本篇文章中,我們將會(huì)對(duì) Java 的運(yùn)行期優(yōu)化摔敛,也就是即時(shí)編譯 (Just In Time, JIT) 時(shí)進(jìn)行的優(yōu)化進(jìn)行詳細(xì)的講解廷蓉,我們先來看看什么是即時(shí)編譯。
即時(shí)編譯
什么是即時(shí)編譯恒削?
- 當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或某段代碼運(yùn)行的特別頻繁時(shí)池颈,會(huì)把這段代碼認(rèn)為成熱點(diǎn)代碼;
- 在運(yùn)行時(shí)钓丰,虛擬機(jī)會(huì)將這段代碼編譯成平臺(tái)相關(guān)的機(jī)器碼躯砰,并進(jìn)行各種層次的優(yōu)化。
HotSpot 虛擬機(jī)內(nèi)的即時(shí)編譯器運(yùn)作過程
我們主要通過以下 5 個(gè)問題來了解 HotSpot 虛擬機(jī)的即時(shí)編譯器。
為什么要使用解釋器與編譯器并存的架構(gòu)?
- 解釋器的優(yōu)點(diǎn):可以提高程序的響應(yīng)速度(省去了編譯的時(shí)間),并且節(jié)約內(nèi)存。
- 編譯器的優(yōu)點(diǎn):可以提高執(zhí)行效率藕溅。
- 虛擬機(jī)參數(shù)設(shè)置:
- 強(qiáng)制運(yùn)行于解析模式:
-Xint
略吨,編譯器完全不工作乞榨; - 強(qiáng)制運(yùn)行于編譯模式:
-Xcomp
,當(dāng)編譯器編譯失敗時(shí),解釋執(zhí)行還是會(huì)介入的。- 混合模式:
-Xmixed
開始解釋執(zhí)行划址,啟動(dòng)速度較快,對(duì)熱點(diǎn)代碼實(shí)行檢測(cè)和編譯
- 混合模式:
- 強(qiáng)制運(yùn)行于解析模式:
為什么虛擬機(jī)要實(shí)現(xiàn)兩個(gè)不同的 JIT 編譯器宜狐?
- Client Compiler(C1):不激進(jìn)優(yōu)化;
- Server Compiler(C2):激進(jìn)優(yōu)化,如果激進(jìn)優(yōu)化不成立,再退回為解釋執(zhí)行或者 C1 編譯器執(zhí)行翼岁。
什么是虛擬機(jī)的分層編譯榆俺?
分層編譯就是根據(jù)編譯器編譯、優(yōu)化的規(guī)模與耗時(shí)付枫,劃分出不同的編譯層次二打,在代碼運(yùn)行的過程中,可以動(dòng)態(tài)的選擇將某一部分代碼片段提升一個(gè)編譯層次或者降低一個(gè)編譯層次掂榔。
C1 與 C2 編譯器會(huì)同時(shí)工作继效,許多代碼可能會(huì)被多次編譯。
目的: 在程序的啟動(dòng)響應(yīng)時(shí)間和運(yùn)行效率間達(dá)到平衡装获。
編譯層次的劃分:
- 第 0 層:解釋執(zhí)行瑞信,不開啟性能監(jiān)控;
- 第 1 層:將字節(jié)編譯為機(jī)器碼穴豫,但不進(jìn)行激進(jìn)優(yōu)化凡简,有必要時(shí)會(huì)加入性能監(jiān)控;
- 第 2 層及以上:將字節(jié)編譯為機(jī)器碼,會(huì)根據(jù)性能監(jiān)控信息進(jìn)行激進(jìn)優(yōu)化秤涩。
如何判斷熱點(diǎn)代碼帜乞,觸發(fā)編譯?
什么是熱點(diǎn)代碼溉仑?
- 被多次調(diào)用的方法挖函;
- 被多次執(zhí)行的循環(huán)體;
- 雖然被判斷為熱點(diǎn)代碼的是循環(huán)體浊竟,不過因?yàn)樘摂M機(jī)的即時(shí)編譯是以方法為單位的怨喘,所以編譯器還是會(huì)將循環(huán)體所在的方法整個(gè)作為編譯對(duì)象。
我們發(fā)現(xiàn)振定,判斷熱點(diǎn)代碼的一個(gè)要點(diǎn)就是: 多次執(zhí)行 必怜。那么虛擬機(jī)是如何知道一個(gè)方法或者一個(gè)循環(huán)體被多次執(zhí)行的呢?
什么是 “多次” 執(zhí)行后频?
-
基于采樣的熱點(diǎn)探測(cè)
- 虛擬機(jī)周期檢查各個(gè)線程的棧頂梳庆,如果發(fā)現(xiàn)一個(gè)方法經(jīng)常出現(xiàn)在棧頂,則該方法為熱點(diǎn)方法卑惜。
- 優(yōu)點(diǎn): 容易獲取方法的調(diào)用關(guān)系膏执,且簡單高效。
- 缺點(diǎn): 無法精準(zhǔn)的判斷一個(gè)方法的熱度露久,并且容易受到線程阻塞的影響更米,如果一個(gè)方法由于它所在的線程被阻塞的緣故而一直出現(xiàn)在棧頂,我們并不能認(rèn)為這個(gè)方法被調(diào)用的十分頻繁毫痕。
-
基于計(jì)數(shù)器的熱點(diǎn)探測(cè)
- 虛擬機(jī)為每一個(gè)方法(或代碼塊)建立一個(gè)計(jì)數(shù)器征峦,一旦執(zhí)行次數(shù)超過一定閾值,就將其判為熱點(diǎn)代碼消请。
- 優(yōu)點(diǎn): 精確嚴(yán)謹(jǐn)栏笆。
- 缺點(diǎn): 不能直接獲取方法的調(diào)用關(guān)系,且實(shí)現(xiàn)復(fù)雜臊泰。
- HotSpot 使用的是這個(gè)蛉加,并且還為每個(gè)方法建立了兩個(gè)計(jì)數(shù)器。
HotSpot 中每個(gè)方法的 2 個(gè)計(jì)數(shù)器
-
方法調(diào)用計(jì)數(shù)器
- 統(tǒng)計(jì)方法被調(diào)用的次數(shù)缸逃,處理多次調(diào)用的方法的七婴。
- 默認(rèn)統(tǒng)計(jì)的不是方法調(diào)用的絕對(duì)次數(shù),而是方法在一段時(shí)間內(nèi)被調(diào)用的次數(shù)察滑,如果超過這個(gè)時(shí)間限制還沒有達(dá)到判為熱點(diǎn)代碼的閾值打厘,則該方法的調(diào)用計(jì)數(shù)器值減半。
- 關(guān)閉熱度衰減:
-XX: -UseCounterDecay
(此時(shí)方法計(jì)數(shù)器統(tǒng)計(jì)的是方法被調(diào)用的絕對(duì)次數(shù))贺辰; - 設(shè)置半衰期時(shí)間:
-XX: CounterHalfLifeTime
(單位是秒)户盯; - 熱度衰減過程是在 GC 時(shí)順便進(jìn)行嵌施。
- 關(guān)閉熱度衰減:
-
回邊計(jì)數(shù)器
- 統(tǒng)計(jì)一個(gè)方法中 “回邊” 的次數(shù),處理多次執(zhí)行的循環(huán)體的莽鸭。
- 回邊:在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令(不是所有循環(huán)體都是回邊吗伤,空循環(huán)體是自己跳向自己,沒有向后跳硫眨,不算回邊)足淆。
- 調(diào)整回邊計(jì)數(shù)器閾值:
-XX: OnStackReplacePercentage
(OSR比率)- Client 模式:
回邊計(jì)數(shù)器的閾值 = 方法調(diào)用計(jì)數(shù)器閾值 * OSR比率 / 100
; - Server 模式:
回邊計(jì)數(shù)器的閾值 = 方法調(diào)用計(jì)數(shù)器閾值 * ( OSR比率 - 解釋器監(jiān)控比率 ) / 100
礁阁;
- Client 模式:
- 統(tǒng)計(jì)一個(gè)方法中 “回邊” 的次數(shù),處理多次執(zhí)行的循環(huán)體的莽鸭。
HotSpot 熱點(diǎn)代碼探測(cè)流程
熱點(diǎn)代碼編譯的過程巧号?
虛擬機(jī)在代碼編譯未完成時(shí)會(huì)按照解釋方式繼續(xù)執(zhí)行,編譯動(dòng)作在后臺(tái)的編譯線程執(zhí)行姥闭。
禁止后臺(tái)編譯:-XX: -BackgroundCompilation
丹鸿,打開后這個(gè)開關(guān)參數(shù)后,交編譯請(qǐng)求的線程會(huì)等待編譯完成棚品,然后執(zhí)行編譯器輸出的本地代碼靠欢。
經(jīng)典優(yōu)化技術(shù)介紹
Content:
- 公共子表達(dá)式消除【語言無關(guān)】
- 數(shù)組范圍檢查消除【語言相關(guān)】
- 方法內(nèi)聯(lián)【最重要】
- 逃逸分析【最前沿】
公共子表達(dá)式消除【語言無關(guān)】
如果一個(gè)表達(dá)式 E 已經(jīng)計(jì)算過了,并且從先前的計(jì)算到現(xiàn)在铜跑,E 中所有變量值都沒有發(fā)生變化门怪,則 E 為公共子表達(dá)式,無需再次計(jì)算锅纺,直接用之前的結(jié)果替換薪缆。
數(shù)組范圍檢查消除【語言相關(guān)】
在循環(huán)中使用循環(huán)變量訪問數(shù)組,如果可以判斷循環(huán)變量的范圍在數(shù)組的索引范圍內(nèi)伞广,則可以消除整個(gè)循環(huán)的數(shù)組范圍檢查
方法內(nèi)聯(lián)【最重要】
目的是:去除方法調(diào)用的成本(如建立棧幀等),并為其他優(yōu)化建立良好的基礎(chǔ)疼电,所以一般將方法內(nèi)兩放在優(yōu)化序列最前端嚼锄,因?yàn)樗鼘?duì)其他優(yōu)化有幫助。
類型繼承關(guān)系分析(Class Hierarchy Analysis蔽豺,CHA)
用于確定在目前已加載的類中区丑,某個(gè)接口是否有多于一種的實(shí)現(xiàn),某個(gè)類是否存在子類修陡、子類是否為抽象類等沧侥。
-
對(duì)于非虛方法:
- 直接進(jìn)行內(nèi)聯(lián),其調(diào)用方法的版本在編譯時(shí)已經(jīng)確定魄鸦,是根據(jù)變量的靜態(tài)類型決定的宴杀。
-
對(duì)于虛方法: (激進(jìn)優(yōu)化,要預(yù)留“逃生門”)
- 向 CHA 查詢此方法在當(dāng)前程序下是否有多個(gè)目標(biāo)可選擇拾因;
- 只有一個(gè)目標(biāo)版本:
- 先對(duì)這唯一的目標(biāo)進(jìn)行內(nèi)聯(lián)旺罢;
- 如果之后的執(zhí)行中旷余,虛擬機(jī)沒有加載到會(huì)令這個(gè)方法接收者的繼承關(guān)系發(fā)生改變的新類,則該內(nèi)聯(lián)代碼可以一直使用扁达;
- 如果加載到導(dǎo)致繼承關(guān)系發(fā)生變化的新類正卧,就拋棄已編譯的代碼。
- 有多個(gè)目標(biāo)版本:
- 使用內(nèi)聯(lián)緩存跪解,未發(fā)生方法調(diào)用前炉旷,內(nèi)聯(lián)緩存為空;
- 第一次調(diào)用發(fā)生后叉讥,記錄調(diào)用方法的對(duì)象的版本信息窘行;
- 之后的每次調(diào)用都要先與內(nèi)聯(lián)緩存中的對(duì)象版本信息進(jìn)行比較;
- 版本信息一樣节吮,繼續(xù)使用內(nèi)聯(lián)代碼抽高;
- 版本信息不一樣,說明程序使用了虛方法的多態(tài)特性透绩,這時(shí)取消內(nèi)聯(lián)翘骂,查找虛方法進(jìn)行方法分派。
- 只有一個(gè)目標(biāo)版本:
- 向 CHA 查詢此方法在當(dāng)前程序下是否有多個(gè)目標(biāo)可選擇拾因;
逃逸分析【最前沿】
基本行為
分析對(duì)象的作用域帚豪,看它有沒有能在當(dāng)前作用域之外使用:
- 方法逃逸:對(duì)象在方法中定義之后碳竟,能被外部方法引用,如作為參數(shù)傳遞到了其他方法中狸臣。
- 線程逃逸:賦值給 static 變量莹桅,或可以在其他線程中訪問的實(shí)例變量。
對(duì)于不會(huì)逃逸到方法或線程外的對(duì)象能進(jìn)行優(yōu)化
-
棧上分配: 對(duì)于不會(huì)逃逸到方法外的對(duì)象烛亦,可以在棧上分配內(nèi)存诈泼,這樣這個(gè)對(duì)象所占用的空間可以隨棧幀出棧而銷毀,減小 GC 的壓力煤禽。
Java虛擬機(jī)中掷贾,在Java堆上分配創(chuàng)建對(duì)象的內(nèi)存空間幾乎是Java程序員都清楚的常識(shí)了捞奕,Java堆中的對(duì)象對(duì)于各個(gè)線程都是共享和可見的,可以回收堆中不再使用的對(duì)象,但回收動(dòng)作無論是篩選可回收對(duì)象轨帜,還是回收和整理內(nèi)存都需要耗費(fèi)時(shí)間.如果確定一個(gè)對(duì)象不會(huì)逃逸出方法之外费尽,那讓這個(gè)對(duì)象在棧上分配內(nèi)存將會(huì)是一個(gè)很不錯(cuò)的主意帽撑,對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀.在一般應(yīng)用中褒搔,不會(huì)逃逸的局部變量所占的比例很大,如果能使用棧上分配恳啥,那大量的對(duì)象就會(huì)隨著方法的結(jié)束而自動(dòng)銷毀了偏灿,垃圾回收系統(tǒng)的壓力將會(huì)小很多.
-
標(biāo)量替換(重要):
- 標(biāo)量:基本數(shù)據(jù)類型和 reference。
- 不創(chuàng)建對(duì)象钝的,而是將對(duì)象拆分成一個(gè)一個(gè)標(biāo)量菩混,然后直接在棧上分配忿墅,是棧上分配的一種實(shí)現(xiàn)方式。
- HotSpot 使用的是標(biāo)量替換而不是棧上分配沮峡,因?yàn)閷?shí)現(xiàn)棧上分配需要更改大量假設(shè)了 “對(duì)象只能在堆中分配” 的代碼疚脐。
標(biāo)量是指一個(gè)數(shù)據(jù)已經(jīng)無法再分解為更小的數(shù)據(jù)來表示了.Java虛擬機(jī)中原始數(shù)據(jù)類型(int,long等數(shù)值類型以及reference類型等)都不能再進(jìn)一步分解,它們就可以稱為標(biāo)量.相對(duì)的邢疙,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解棍弄,那么它就稱作聚合量,Java中的對(duì)象就是最典型的聚合量.如果把一個(gè)Java對(duì)象拆散疟游,根據(jù)程序訪問的情況呼畸,將其使用到的成員變量恢復(fù)原始數(shù)據(jù)來訪問就叫做標(biāo)量替換.如果逃逸分析證明一個(gè)對(duì)象不會(huì)被外部訪問,并且這個(gè)對(duì)象可以被拆散的話颁虐,那程序真正執(zhí)行的時(shí)候?qū)⒖赡懿粍?chuàng)建這個(gè)對(duì)象蛮原,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來代替.將對(duì)象拆分后,除了可以讓對(duì)象的成員變量在棧上(棧上存儲(chǔ)的數(shù)據(jù)另绩,有很大概率會(huì)被虛擬機(jī)分配至物理機(jī)器的高速寄存器中存儲(chǔ))分配和讀寫之外儒陨,還可以為后續(xù)進(jìn)一步的優(yōu)化手段創(chuàng)建條件.
-
鎖消除: 不會(huì)逃逸到線程外的方法不需要進(jìn)行同步。
線程同步本身是一個(gè)相對(duì)耗時(shí)的過程笋籽,如果逃逸分析能夠確定一個(gè)變量不會(huì)逃逸出線程蹦漠,無法被其他線程訪問,那這個(gè)變量的讀寫肯定就不會(huì)有競(jìng)爭车海,對(duì)這個(gè)變量實(shí)施的同步措施也就可以消除掉.
虛擬機(jī)參數(shù)
- 開啟逃逸分析:
-XX: +DoEscapeAnalysis
- 開啟標(biāo)量替換:
-XX: +EliminateAnalysis
- 開啟鎖消除:
-XX: +EliminateLocks
- 查看分析結(jié)果:
-XX: PrintEscapeAnalysis
- 查看標(biāo)量替換情況:
-XX: PrintEliminateAllocations
一個(gè)優(yōu)化的例子
原始代碼:
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.get();
// ...do stuff...
z = b.get();
sum = y + z;
}
第一步優(yōu)化: 方法內(nèi)聯(lián)(一般放在優(yōu)化序列最前端笛园,因?yàn)閷?duì)其他優(yōu)化有幫助)
目的:
- 去除方法調(diào)用的成本(如建立棧幀等)
- 為其他優(yōu)化建立良好的基礎(chǔ)
public void foo() {
y = b.value;
// ...do stuff...
z = b.value;
sum = y + z;
}
第二步優(yōu)化: 公共子表達(dá)式消除
public void foo() {
y = b.value;
// ...do stuff... // 因?yàn)檫@部分并沒有改變 b.value 的值
// 如果把 b.value 看成一個(gè)表達(dá)式,就是公共表達(dá)式消除
z = y; // 把這一步的 b.value 替換成 y
sum = y + z;
}
第三步優(yōu)化: 復(fù)寫傳播
public void foo() {
y = b.value;
// ...do stuff...
y = y; // z 變量與以相同侍芝,完全沒有必要使用一個(gè)新的額外變量
// 所以將 z 替換為 y
sum = y + z;
}
第四步優(yōu)化: 無用代碼消除
無用代碼:
- 永遠(yuǎn)不會(huì)執(zhí)行的代碼
- 完全沒有意義的代碼
public void foo() {
y = b.value;
// ...do stuff...
// y = y; 這句沒有意義的研铆,去除
sum = y + y;
}
逃逸分析實(shí)戰(zhàn)
實(shí)驗(yàn)準(zhǔn)備
- 強(qiáng)制開啟逃逸分析(JVM默認(rèn)開啟太醫(yī))
-XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m
- 關(guān)閉逃逸分析
-XX:-DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m
代碼實(shí)例
public class OnStackTest {
public static void alloc() {
byte[] b = new byte[2];
b[0] = 1;
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e - b);
}
}
實(shí)驗(yàn)結(jié)果
-
開啟逃逸的運(yùn)行結(jié)果
-
關(guān)閉逃逸的運(yùn)行結(jié)果
實(shí)驗(yàn)分析
分析一下,這里是將2個(gè)字節(jié)的數(shù)據(jù)循環(huán)分配1千萬次州叠,開啟逃逸的運(yùn)行時(shí)間為28milisecond棵红, 而未開啟則為2726, 為未開啟的將近1/100.
差異效果還是非常明顯的…..
實(shí)驗(yàn)總結(jié)
棧上的空間一般而言是非常小的留量,只能存放若干變化和小的數(shù)據(jù)結(jié)構(gòu),大容量的存儲(chǔ)結(jié)構(gòu)是做不到哟冬。這里的例子是一個(gè)極端的千萬次級(jí)的循環(huán)楼熄,突出了通過逃逸分析,讓其直接從棧上分配浩峡,從而極大降低了GC的次數(shù)可岂,提升了程序整體的執(zhí)行效能。
所以翰灾,逃逸分析的效果只能在特定場(chǎng)景下缕粹,滿足高頻和高數(shù)量的容量比較小的變量分配結(jié)構(gòu)稚茅,才可以生效。
轉(zhuǎn)載于
- https://github.com/TangBean/understanding-the-jvm/blob/master/Ch4-Java%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E4%BC%98%E5%8C%96/00-Java%E8%BF%90%E8%A1%8C%E6%9C%9F%E4%BC%98%E5%8C%96.md#%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90%E6%9C%80%E5%89%8D%E6%B2%BF
- https://blog.csdn.net/blueheart20/article/details/76167489