簡介
小師妹已經(jīng)學(xué)完JVM的簡單部分了管毙,接下來要進(jìn)入的是JVM中比較晦澀難懂的概念背桐,這些概念是那么的枯燥乏味关拒,甚至還有點惹人討厭请契,但是要想深入理解JVM,這些概念是必須的夏醉,我將會盡量嘗試用簡單的例子來解釋它們爽锥,但一定會有人看不懂,沒關(guān)系畔柔,這個系列本不是給所有人看的氯夷。
更多精彩內(nèi)容且看:
- 區(qū)塊鏈從入門到放棄系列教程-涵蓋密碼學(xué),超級賬本,以太坊,Libra,比特幣等持續(xù)更新
- Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續(xù)更新
- Spring 5.X系列教程:滿足你對Spring5的一切想象-持續(xù)更新
- java程序員從小工到專家成神之路(2020版)-持續(xù)更新中,附詳細(xì)文章教程
JIT編譯器
小師妹:F師兄,我的基礎(chǔ)已經(jīng)打牢了嗎靶擦?可以進(jìn)入這么復(fù)雜的內(nèi)容環(huán)節(jié)了嗎腮考?
小師妹不試試怎么知道不行呢?了解點深入內(nèi)容可以幫助你更好的理解之前的知識⌒叮現(xiàn)在我們開始吧踩蔚。
上次我們在講java程序的處理流程的時候,還記得那通用的幾步吧枚粘。
小師妹:當(dāng)然記得了馅闽,編寫源代碼,javac編譯成字節(jié)碼,加載到JVM中執(zhí)行福也。
對局骤,其實在JVM的執(zhí)行引擎中,有三個部分:解釋器暴凑,JIT編譯器和垃圾回收器峦甩。
解釋器會將前面編譯生成的字節(jié)碼翻譯成機器語言,因為每次都要翻譯现喳,相當(dāng)于比直接編譯成機器碼要多了一步凯傲,所以java執(zhí)行起來會比較慢。
為了解決這個問題嗦篱,JVM引入了JIT(Just-in-Time)編譯器泣洞,將熱點代碼編譯成為機器碼。
Tiered Compilation分層編譯
小師妹你知道嗎默色?在JDK8之前球凰,HotSpot VM又分為三種。分別是 client VM, server VM, 和 minimal VM腿宰,分別用在客戶端呕诉,服務(wù)器,和嵌入式系統(tǒng)吃度。
但是隨著硬件技術(shù)的發(fā)展甩挫,這些硬件上面的限制都不是什么大事了。所以從JDK8之后椿每,已經(jīng)不再區(qū)分這些VM了伊者,現(xiàn)在統(tǒng)一使用VM的實現(xiàn)來替代他們。
小師妹间护,你覺得Client VM和Server VM的本質(zhì)區(qū)別在哪一部分呢亦渗?
小師妹,編譯成字節(jié)碼應(yīng)該都是使用javac汁尺,都是同樣的命令法精,字節(jié)碼上面肯定是一樣的。難點是在執(zhí)行引擎上面的不同痴突?
說的對搂蜓,因為Client VM和Server VM的出現(xiàn),所以在JIT中出現(xiàn)了兩種不同的編譯器辽装,C1 for Client VM帮碰, C2 for Server VM。
因為javac的編譯只能做少量的優(yōu)化拾积,其實大量的動態(tài)優(yōu)化是在JIT中做的殉挽。C2相對于C1丰涉,其優(yōu)化的程度更深,更加激進(jìn)此再。
為了更好的提升編譯效率,JVM在JDK7中引入了分層編譯Tiered compilation的概念玲销。
對于JIT本身來說输拇,動態(tài)編譯是需要占用用戶內(nèi)存空間的,有可能會造成較高的延遲贤斜。
對于Server服務(wù)器來說策吠,因為代碼要服務(wù)很多個client,所以磨刀不誤砍柴工瘩绒,短暫的延遲帶來永久的收益猴抹,聽起來是可以接受的。
Server端的JIT編譯也不是立馬進(jìn)行的锁荔,它可能需要收集到足夠多的信息之后蟀给,才進(jìn)行編譯。
而對于Client來說阳堕,延遲帶來的性能影響就需要進(jìn)行考慮了跋理。和Server相比,它只進(jìn)行了簡單的機器碼的編譯恬总。
為了滿足不同層次的編譯需求前普,于是引入了分層編譯的概念。
大概來說分層編譯可以分為三層:
- 第一層就是禁用C1和C2編譯器壹堰,這個時候沒有JIT進(jìn)行拭卿。
- 第二層就是只開啟C1編譯器,因為C1編譯器只會進(jìn)行一些簡單的JIT優(yōu)化贱纠,所以這個可以應(yīng)對常規(guī)情況峻厚。
- 第三層就是同時開啟C1和C2編譯器。
在JDK7中谆焊,你可以使用下面的命令來開啟分層編譯:
-XX:+TieredCompilation
而在JDK8之后目木,恭喜你,分層編譯已經(jīng)是默認(rèn)的選項了懊渡,不用再手動開啟刽射。
OSR(On-Stack Replacement)
小師妹:F師兄,你剛剛講到Server的JIT不是立馬就進(jìn)行編譯的剃执,它會等待一定的時間來搜集所需的信息誓禁,那么代碼不是要從字節(jié)碼轉(zhuǎn)換成機器碼?
對的肾档,這個過程就叫做OSR(On-Stack Replacement)摹恰。為什么叫OSR呢辫继?我們知道JVM的底層實現(xiàn)是一個棧的虛擬機,所以這個替換實際上是一系列的Stack操作俗慈。
上圖所示姑宽,m1方法從最初的解釋frame變成了后面的compiled frame。
Deoptimization
這個世界是平衡的闺阱,有陰就有陽炮车,有優(yōu)化就有反優(yōu)化。
小師妹:F師兄酣溃,為什么優(yōu)化了之后還要反優(yōu)化呢瘦穆?這樣對性能不是下降了嗎?
通常來說是這樣的赊豌,但是有些特殊的情況下面扛或,確實是需要進(jìn)行反優(yōu)化的。
下面是比較常見的情況:
- 需要調(diào)試的情況
如果代碼正在進(jìn)行單個步驟的調(diào)試碘饼,那么之前被編譯成為機器碼的代碼需要反優(yōu)化回來熙兔,從而能夠調(diào)試。
- 代碼廢棄的情況
當(dāng)一個被編譯過的方法艾恼,因為種種原因不可用了黔姜,這個時候就需要將其反優(yōu)化。
- 優(yōu)化之前編譯的代碼
有可能出現(xiàn)之前優(yōu)化過的代碼可能不夠完美蒂萎,需要重新優(yōu)化的情況秆吵,這種情況下同樣也需要進(jìn)行反優(yōu)化。
常見的編譯優(yōu)化舉例
除了JIT編譯成機器碼之外五慈,JIT還有一下常見的代碼優(yōu)化方式纳寂,我們來一一介紹。
Inlining內(nèi)聯(lián)
舉個例子:
int a = 1;
int b = 2;
int result = add(a, b);
...
public int add(int x, int y) { return x + y; }
int result = a + b; //內(nèi)聯(lián)替換
上面的add方法可以簡單的被替換成為內(nèi)聯(lián)表達(dá)式泻拦。
Branch Prediction分支預(yù)測
通常來說對于條件分支毙芜,因為需要有一個if的判斷條件,JVM需要在執(zhí)行完畢判斷條件争拐,得到返回結(jié)果之后腋粥,才能夠繼續(xù)準(zhǔn)備后面的執(zhí)行代碼,如果有了分支預(yù)測架曹,那么JVM可以提前準(zhǔn)備相應(yīng)的執(zhí)行代碼隘冲,如果分支檢查成功就直接執(zhí)行,省去了代碼準(zhǔn)備的步驟绑雄。
比如下面的代碼:
// make an array of random doubles 0..1
double[] bigArray = makeBigArray();
for (int i = 0; i < bigArray.length; i++)
{
double cur = bigArray[i];
if (cur > 0.5) { doThis();} else { doThat();}
}
Loop unswitching
如果我們在循環(huán)語句里面添加了if語句展辞,為了提升并發(fā)的執(zhí)行效率,可以將if語句從循環(huán)中提取出來:
int i, w, x[1000], y[1000];
for (i = 0; i < 1000; i++) {
x[i] += y[i];
if (w)
y[i] = 0;
}
可以改為下面的方式:
int i, w, x[1000], y[1000];
if (w) {
for (i = 0; i < 1000; i++) {
x[i] += y[i];
y[i] = 0;
}
} else {
for (i = 0; i < 1000; i++) {
x[i] += y[i];
}
}
Loop unrolling展開
在循環(huán)語句中万牺,因為要不斷的進(jìn)行跳轉(zhuǎn)罗珍,所以限制了執(zhí)行的速度洽腺,我們可以對循環(huán)語句中的邏輯進(jìn)行適當(dāng)?shù)恼归_:
int x;
for (x = 0; x < 100; x++)
{
delete(x);
}
轉(zhuǎn)變?yōu)椋?/p>
int x;
for (x = 0; x < 100; x += 5 )
{
delete(x);
delete(x + 1);
delete(x + 2);
delete(x + 3);
delete(x + 4);
}
雖然循環(huán)體變長了,但是跳轉(zhuǎn)次數(shù)變少了覆旱,其實是可以提升執(zhí)行速度的蘸朋。
Escape analysis逃逸分析
什么叫逃逸分析呢?簡單點講就是分析這個線程中的對象扣唱,有沒有可能會被其他對象或者線程所訪問藕坯,如果有的話,那么這個對象應(yīng)該在Heap中分配画舌,這樣才能讓對其他的對象可見堕担。
如果沒有其他的對象訪問已慢,那么完全可以在stack中分配這個對象曲聂,棧上分配肯定比堆上分配要快,因為不用考慮同步的問題佑惠。
我們舉個例子:
public static void main(String[] args) {
example();
}
public static void example() {
Foo foo = new Foo(); //alloc
Bar bar = new Bar(); //alloc
bar.setFoo(foo);
}
}
class Foo {}
class Bar {
private Foo foo;
public void setFoo(Foo foo) {
this.foo = foo;
}
}
上面的例子中朋腋,setFoo引用了foo對象,如果bar對象是在heap中分配的話膜楷,那么引用的foo對象就逃逸了旭咽,也需要被分配在heap空間中。
但是因為bar和foo對象都只是在example方法中調(diào)用的赌厅,所以穷绵,JVM可以分析出來沒有其他的對象需要引用他們,那么直接在example的方法棧中分配這兩個對象即可特愿。
逃逸分析還有一個作用就是lock coarsening仲墨。
為了在多線程環(huán)境中保證資源的有序訪問,JVM引入了鎖的概念揍障,雖然鎖可以保證多線程的有序執(zhí)行目养,但是如果實在單線程環(huán)境中呢?是不是還需要一直使用鎖呢毒嫡?
比如下面的例子:
public String getNames() {
Vector<String> v = new Vector<>();
v.add("Me");
v.add("You");
v.add("Her");
return v.toString();
}
Vector是一個同步對象癌蚁,如果是在單線程環(huán)境中,這個同步鎖是沒有意義的兜畸,因此在JDK6之后努释,鎖只在被需要的時候才會使用。
這樣就能提升程序的執(zhí)行效率咬摇。
總結(jié)
本文介紹了JIT的原理和一些基本的優(yōu)化方式洽洁。后面我們會繼續(xù)探索JIT和JVM的秘密,敬請期待菲嘴。
本文作者:flydean程序那些事
本文鏈接:http://www.flydean.com/jvm-jit-in-detail/
本文來源:flydean的博客
歡迎關(guān)注我的公眾號:程序那些事饿自,更多精彩等著您汰翠!