類執(zhí)行機(jī)制
在完成將class文件信息加載到JVM并產(chǎn)生Class對象后绍坝,就可執(zhí)行Class對象的靜態(tài)方法或?qū)嵗瘜ο筮M(jìn)行調(diào)用了。
字節(jié)碼解釋執(zhí)行方式 在源碼編譯階段將源碼編譯為JVM字節(jié)碼浅浮,JVM字節(jié)碼是一種中間代碼的方式掐场,要由JVM在運(yùn)行期對其進(jìn)行解釋并執(zhí)行桩引。
字節(jié)碼解釋執(zhí)行
由于采用的為中間碼的方式,JVM有一套自己的指令菌瘫,對于面向?qū)ο蟮恼Z言而言蜗顽,最重要的是執(zhí)行方法的指令,JVM采用四個指令來執(zhí)行不同的方法調(diào)用:
-
invokestatic
對應(yīng)的是調(diào)用static方法 -
invokevirtual
對應(yīng)的是調(diào)用對象實例的方法 -
invokeinterface
對應(yīng)的是調(diào)用接口的方法 -
invokespecial
對應(yīng)的是調(diào)用private方法和編譯源碼后生成的方法雨让,此方法為對象實例化時的初始化方法
下面一段代碼經(jīng)過javac
編譯后雇盖,查看其字節(jié)碼可以看到上面四種方法的調(diào)用。
public class Demo {
public void execute() {
A.execute();
A a = new A();
a.bar();
IFoo b=new B();
b.bar();
}
// Class A
static class A {
public static int execute() {
return 1+2;
}
public int bar() {
return 1+2;
}
}
// Class B
class B implements IFoo {
public int bar(){
return 1+2;
}
}
public interface IFoo {
public int bar();
}
}
javac Demo.java # 生成class文件
javap -c Demo # 查看execute執(zhí)行的字節(jié)碼
JDK 是基于棧的體系結(jié)構(gòu)來執(zhí)行字節(jié)碼栖忠,我們在創(chuàng)建線程的時候崔挖,都會產(chǎn)生程序計數(shù)器(PC)(或稱為PC registers)和棧(Stack)。
- PC存放了下一條要執(zhí)行的指令在方法內(nèi)的偏移量庵寞;
- 棧中存放了棧幀(StackFrame)狸相;
每個方法每次調(diào)用都會產(chǎn)生棧幀。棧幀主要分為局部變量區(qū)和操作數(shù)棧兩部分:
- 局部變量區(qū)用于存放方法中的局部變量和參數(shù)
- 操作數(shù)棧中用于存放方法執(zhí)行過程中產(chǎn)生的中間結(jié)果
- 棧幀中還會有一些雜用空間捐川,例如指向方法已解析的常量池的引用脓鹃、其他一些VM內(nèi)部實現(xiàn)需要的數(shù)據(jù)
例如下面的代碼:
public class DemoTwo {
public static void execute() {
int a = 1;
int b = 2;
int c = (a+b)*5;
}
}
方法執(zhí)行的過程如下:
Compiled from "DemoTwo.java"
public class DemoTwo {
public DemoTwo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void execute();
Code:
0: iconst_1 // 將類型為int、值為1的常量放入操作數(shù)棧古沥;
1: istore_0 // 將操作數(shù)棧中棧頂?shù)闹祻棾龇湃刖植孔兞繀^(qū)瘸右;
2: iconst_2 // 將類型為int、值為2的常量放入操作數(shù)棧岩齿;
3: istore_1 // 將操作數(shù)棧中棧頂?shù)闹祻棾龇湃刖植孔兞繀^(qū)太颤;
4: iload_0 // 裝載局部變量區(qū)中的第一個值到操作數(shù)棧;
5: iload_1 // 裝載局部變量區(qū)中的第二個值到操作數(shù)棧盹沈;
6: iadd // 執(zhí)行int類型的add指令栋齿,并將計算出的結(jié)果放入操作數(shù)棧;
7: iconst_5 // 將類型為int襟诸、值為5的常量放入操作數(shù)棧;
8: imul // 執(zhí)行int類型的mul指令基协,并將計算出的結(jié)果放入操作數(shù)棧歌亲;
9: istore_2 // 將操作數(shù)棧中棧頂?shù)闹祻棾龇湃刖植孔兞繀^(qū);
10: return
}
編譯執(zhí)行
解釋執(zhí)行的效率較低澜驮,為提升代碼的執(zhí)行性能陷揪,Sun JDK提供將字節(jié)碼編譯為機(jī)器碼的支持,編譯在運(yùn)行時進(jìn)行,通常稱為JIT編譯器悍缠。
Sun JDK在執(zhí)行過程中對執(zhí)行頻率高的代碼進(jìn)行編譯卦绣,對執(zhí)行不頻繁的代碼則繼續(xù)采用解釋的方式,因此SunJDK又稱為Hotspot VM飞蚓。
在編譯上Sun JDK提供了兩種模式:
- client compiler (-client)
- server compiler (-server)
client compiler (C1)
又稱為C1滤港,較為輕量級,只做少量性能開銷比高的優(yōu)化趴拧,它占用內(nèi)存較少溅漾,適合于桌面交互式應(yīng)用。在寄存器分配策略上著榴,JDK 6以后采用的為線性掃描寄存器分配算法添履,在其他方面的優(yōu)化主要有:方法內(nèi)聯(lián)、去虛擬化脑又、冗余削除等暮胧。
方法內(nèi)聯(lián)
方法執(zhí)行時,要經(jīng)歷多次參數(shù)傳遞问麸、返回值傳遞及跳轉(zhuǎn)等往衷,于是C1采取了方法內(nèi)聯(lián)的方式,即把調(diào)用到的方法的指令直接植入當(dāng)前方法中口叙。
如下代碼:
public void bar() {
…
bar2();
…
}
private void bar2() {
// do something
}
當(dāng)編譯時炼绘,如bar2代碼編譯后的字節(jié)數(shù)小于等于35字節(jié),那么妄田,會演變成類似這樣的結(jié)構(gòu):
35K
這個值可通過在啟動參數(shù)中增加-XX:MaxInlineSize=35
來進(jìn)行控制俺亮。
public void bar() {
…
// do something
…
}
去虛擬化
去虛擬化是指在裝載class文件后,進(jìn)行類層次的分析疟呐,如發(fā)現(xiàn)類中的方法只提供一個實現(xiàn)類脚曾,那么對于調(diào)用了此方法的代碼,也可進(jìn)行方法內(nèi)聯(lián)启具,從而提升執(zhí)行的性能本讥。
如下代碼:
public interface IFoo {
public void bar();
}
public class Foo implements IFoo {
public void bar() {
// do something
}
}
public class Demo {
public void execute(IFoo foo) {
foo.bar();
}
}
當(dāng)整個JVM中只有Foo實現(xiàn)了IFoo接口,Demo execute方法被編譯時鲁冯,就演變成類似這樣的結(jié)構(gòu):
public void execute() {
// do something
}
冗余消除
冗余削除是指在編譯時拷沸,根據(jù)運(yùn)行時狀況進(jìn)行代碼折疊或削除。
例如一段這樣的代碼:
private static final Log log = LogFactory.getLog(“BLUEDAVY”);
public void execute(){
if(log.isDebugEnabled()){
log.debug(“enter this method: execute”);
}
// do something
}
如log.isDebugEnabled返回的為false薯演,在執(zhí)行C1編譯后撞芍,這段代碼就演變成類似下面的結(jié)構(gòu):
public void execute() {
// do something
}
這是為什么會在有些代碼編寫規(guī)則上寫不要直接調(diào)用log.debug,而要先判斷的原因跨扮。
server compiler(C2)
又稱為C2序无,較為重量級验毡,C2采用了大量的傳統(tǒng)編譯優(yōu)化技巧來進(jìn)行優(yōu)化,占用內(nèi)存相對會多一些帝嗡,適合于服務(wù)器端的應(yīng)用晶通。
和C1不同的主要是寄存器分配策略及優(yōu)化的范圍,寄存器分配策略上C2采用的為傳統(tǒng)的圖著色寄存器分配算法哟玷;由于C2會收集程序的運(yùn)行信息狮辽,因此其優(yōu)化的范圍更多在于全局的優(yōu)化,而不僅僅是一個方法塊的優(yōu)化碗降。收集的信息主要有:分支的跳轉(zhuǎn)/不跳轉(zhuǎn)的頻率隘竭、某條指令上出現(xiàn)過的類型、是否出現(xiàn)過空值讼渊、是否出現(xiàn)過異常动看。
逃逸分析 是C2進(jìn)行很多優(yōu)化的基礎(chǔ),逃逸分析是指根據(jù)運(yùn)行狀況來判斷方法中的變量是否會被外部讀取爪幻。如不會則認(rèn)為此變量是逃逸的菱皆,基于逃逸分析C2在編譯時會做標(biāo)量替換、棧上分配和同步削除等優(yōu)化挨稿。
在6.0的逃逸分析實現(xiàn)上有些影響性能仇轻,因此在update 18里臨時禁用了,在Java 7中則默認(rèn)打開奶甘。
標(biāo)量替換
標(biāo)量替換的意思簡單來說就是用標(biāo)量替換聚合量篷店。
例如如下代碼:
Point point=new Point(1,2);
System.out.println(“point.x=”+point.x+”; point.y=”+point.y);
當(dāng)point對象在后面的執(zhí)行過程中未用到時,經(jīng)過編譯后臭家,代碼會變成類似下面的結(jié)構(gòu):
int x=1;
int y=2;
System.out.println(“point.x=”+x+”; point.y=”+y);
之后基于此可以繼續(xù)做冗余削除疲陕。這種方式能帶來的好處是,如果創(chuàng)建的對象并未用到其中的全部變量钉赁,則可以節(jié)省一定的內(nèi)存蹄殃。而對于代碼執(zhí)行而言,由于無須去找對象的引用你踩,也會更快一些诅岩。
棧上分配
在上面的例子中,如果p沒有逃逸带膜,那么C2會選擇在棧上直接創(chuàng)建Point對象實例吩谦,而不是在JVM堆上。在棧上分配的好處一方面是更加快速膝藕,另一方面是回收時隨著方法的結(jié)束逮京,對象也被回收了,這也是棧上分配的概念束莫。
同步削除
同步削除是指如果發(fā)現(xiàn)同步的對象未逃逸懒棉,那也沒有同步的必要了,在C2編譯時會直接去掉同步览绿。
例如如下代碼:
Point point=new Point(1,2);
synchronized(point){
// do something
}
經(jīng)過分析如果發(fā)現(xiàn)point未逃逸策严,在編譯后,代碼就會變成下面的結(jié)構(gòu):
Point point=new Point(1,2);
// do something
除了基于逃逸分析的這些外饿敲,C2還會基于其擁有的運(yùn)行信息來做其他的優(yōu)化妻导,例如編譯分支頻率執(zhí)行高的代碼等。
OSR編譯
OSR編譯和C1怀各、C2最主要的不同點在于OSR編譯只替換循環(huán)代碼體的入口倔韭,而C1、C2替換的是方法調(diào)用的入口瓢对,因此在OSR編譯后會出現(xiàn)的現(xiàn)象是方法的整段代碼被編譯了寿酌,但只有在循環(huán)代碼體部分才執(zhí)行編譯后的機(jī)器碼,其他部分則仍然是解釋執(zhí)行方式硕蛹。
默認(rèn)情況下醇疼,Sun JDK根據(jù)機(jī)器配置來選擇client或server模式,當(dāng)機(jī)器配置CPU超過2核且內(nèi)存超過2GB即默認(rèn)為server模式法焰,但在32位Windows機(jī)器上始終選擇的都是client模式時秧荆,也可在啟動時通過增加-client或-server來強(qiáng)制指定
Sun JDK之所以未選擇在啟動時即編譯成機(jī)器碼,有幾方面的原因:
- 靜態(tài)編譯并不能根據(jù)程序的運(yùn)行狀況來優(yōu)化執(zhí)行的代碼埃仪,C2這種方式是根據(jù)運(yùn)行狀況來進(jìn)行動態(tài)編譯的乙濒,例如分支判斷、逃逸分析等卵蛉,這些措施會對提升程序執(zhí)行的性能會起到很大的幫助颁股,在靜態(tài)編譯的情況下是無法實現(xiàn)的。給C2收集運(yùn)行數(shù)據(jù)越長的時間毙玻,編譯出來的代碼會越優(yōu)豌蟋;
- 解釋執(zhí)行比編譯執(zhí)行更節(jié)省內(nèi)存;
- 啟動時解釋執(zhí)行的啟動速度比編譯再啟動更快桑滩。
但程序在未編譯期間解釋執(zhí)行方式會比較慢梧疲,因此需要取一個權(quán)衡值,在Sun JDK中主要依據(jù)方法上的兩個計數(shù)器是否超過閾值运准,其中一個計數(shù)器為調(diào)用計數(shù)器幌氮,即方法被調(diào)用的次數(shù);另一個計數(shù)器為回邊計數(shù)器胁澳,即方法中循環(huán)執(zhí)行部分代碼的執(zhí)行次數(shù)该互。
調(diào)用計數(shù)器:CompileThreshold
該值是指當(dāng)方法被調(diào)用多少次后,就編譯為機(jī)器碼韭畸。在client模式下默認(rèn)為1 500次宇智,在server模式下默認(rèn)為10000次蔓搞,可通過在啟動時添加-XX:CompileThreshold=10 000
來設(shè)置該值。
-XX:CompileThreshold=10 000
回邊計數(shù)器:OnStackReplacePercentage
該值為用于計算是否觸發(fā)OSR編譯的閾值随橘,默認(rèn)情況下client模式時為933喂分,server模式下為140,該值可通過在啟動時添加-XX:OnStackReplacePercentage=140
來設(shè)置机蔗。
client模式下計算規(guī)則:
CompileThreshold*(OnStackReplacePercentage/100)
server模式下計算規(guī)則:
(CompileThreshold*(OnStackReplacePercentage-InterpreterProfilePercentage))/100
Interpreter-ProfilePercentage的默認(rèn)值為33蒲祈,當(dāng)方法上的回邊計數(shù)器到達(dá)這個值時,即觸發(fā)后臺的OSR編譯萝嘁,并將方法上累積的調(diào)用計數(shù)器設(shè)置為Com-pileThreshold的值梆掸,同時將回邊計數(shù)器設(shè)置為CompileThreshold/2的值,一方面是為了避免OSR編譯頻繁觸發(fā)牙言;另一方面是以便當(dāng)方法被再次調(diào)用時即觸發(fā)正常的編譯酸钦,當(dāng)累積的回邊計數(shù)器的值再次達(dá)到該值時,先檢查OSR編譯是否完成嬉挡。