類執(zhí)行機(jī)制

類執(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é)碼

類字節(jié)碼
類字節(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ù)
JDK基于棧的體系結(jié)構(gòu)
JDK基于棧的體系結(jié)構(gòu)

例如下面的代碼:

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ī)器碼,有幾方面的原因:

  1. 靜態(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)豌蟋;
  2. 解釋執(zhí)行比編譯執(zhí)行更節(jié)省內(nèi)存;
  3. 啟動時解釋執(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編譯是否完成嬉挡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钝鸽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子庞钢,更是在濱河造成了極大的恐慌拔恰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件基括,死亡現(xiàn)場離奇詭異颜懊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)风皿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門河爹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桐款,你說我怎么就攤上這事咸这。” “怎么了魔眨?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵媳维,是天一觀的道長。 經(jīng)常有香客問我遏暴,道長侄刽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任朋凉,我火速辦了婚禮州丹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己墓毒,他們只是感情好吓揪,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚁鳖,像睡著了一般磺芭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上醉箕,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機(jī)與錄音徙垫,去河邊找鬼讥裤。 笑死,一個胖子當(dāng)著我的面吹牛姻报,可吹牛的內(nèi)容都是我干的己英。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吴旋,長吁一口氣:“原來是場噩夢啊……” “哼损肛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荣瑟,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤治拿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后笆焰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體劫谅,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年嚷掠,在試婚紗的時候發(fā)現(xiàn)自己被綠了捏检。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡不皆,死狀恐怖贯城,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情霹娄,我是刑警寧澤能犯,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站项棠,受9級特大地震影響悲雳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜香追,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一合瓢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧透典,春花似錦晴楔、人聲如沸顿苇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纪岁。三九已至,卻和暖如春则果,著一層夾襖步出監(jiān)牢的瞬間幔翰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工西壮, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留遗增,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓款青,卻偏偏與公主長得像做修,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抡草,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容