JVM 方法到底如何執(zhí)行

前言

JVM內(nèi)存模型.png

與上圖類似的JVM內(nèi)存模型圖見過多次,僅從概念上去理解各個(gè)區(qū)域的作用,難有深刻印象。

當(dāng)學(xué)習(xí)一個(gè)類如何存儲(chǔ),即JVM如何解析.Class文件,能知道方法區(qū)存在的意義。本文的目的則是學(xué)習(xí)JVM如何執(zhí)行一個(gè)方法,如此對(duì)棧與程序計(jì)數(shù)器有更深刻的認(rèn)識(shí)。

note 文中部分內(nèi)容需要.Class文件知識(shí),但總體上不妨礙理解
.Class參考

字節(jié)碼基礎(chǔ)

Java代碼通過編譯后,會(huì)將對(duì)應(yīng)的函數(shù)方法轉(zhuǎn)為字節(jié)碼指令,如果了解.Class如何組成,可在對(duì)應(yīng)方法表里的Code屬性表查找到對(duì)應(yīng)的一系列字節(jié)碼指令。函數(shù)的執(zhí)行本質(zhì)上是數(shù)據(jù)運(yùn)算與執(zhí)行調(diào)度,因此可以用一系列的指令來進(jìn)行描述。

字節(jié)碼指令由一個(gè)字節(jié)長(zhǎng)度表示,代表特定的操作含義,后面可以跟隨零到多個(gè)必要參數(shù)。使用一個(gè)字節(jié)表示,意味著字節(jié)碼的總數(shù)不可能操作 256條。

下面表列出了常用的數(shù)據(jù)類型對(duì)應(yīng)的字節(jié)碼指令,粗略看一眼就可以,需要的時(shí)候再具體查閱每條字節(jié)碼的含義。

opcode byte short int long float doubt char Reference
Tipush bipush sipush
Tconst iconst lconst fconst dconst aconst
Tload iload lload fload dload alod
Tstore istore lstore fstore dstore astore
Tinc iinc
Taload baload saload iaload laload faload daload caload aaload
Tastore bastore sastore iastore lastore fastore dastore castore astore
Tadd iadd ladd fadd dadd
Tsub isub lsub fsub dsub
Tmul imul lmul fmul dmul
Tdiv idvi ldiv fdiv ddiv
Trem irem lrem frem drem
Tneg ineg lneg fneg dneg
Tncg ineg lneg fneg dneg
Tshl ishl lshl
Tshr ishr lshr
Tushr iushr lushr
Tadnd iadn land
Tor ior lor
Txor ixor lxor
i2T i2b i2s i2l i2f i2d
l2T l2i l2f l2d
f2T f2i f2l F2d
d2T d2i d2l D2f
Tcmp lamp
Tcmpl fcmpl dcmpl
Tcmpg fcmpg dcmpg
if_TcmpOP if_icmpOP if_acmpOP
Treturn ireturn lreturn freturn dreturn Return

字節(jié)碼含義可參考此文

字節(jié)碼用途大致分為9類,僅做簡(jiǎn)要介紹:

  • 加載和存儲(chǔ)指令:用于將數(shù)據(jù)在棧幀中的局部變量表和操作數(shù)棧之間來回傳輸,如將一個(gè)局部變量加載到操作數(shù)棧Tload; 將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表Tstore
  • 運(yùn)算指令:用于對(duì)兩個(gè)操作數(shù)棧上的值進(jìn)行某種特定運(yùn)算,并把結(jié)果重新存入到操作棧頂,如加減對(duì)應(yīng)為Tadd、Tsub
  • 類型轉(zhuǎn)換指令:將兩種不同的數(shù)值進(jìn)行相互轉(zhuǎn)換
  • 對(duì)象創(chuàng)建與訪問指令: 創(chuàng)建類實(shí)例指令new,訪問類字段getfield、putfield
  • 操作數(shù)棧管理指令:與操作數(shù)據(jù)結(jié)構(gòu)的堆棧類似,如將操作數(shù)棧棧頂一個(gè)或兩個(gè)元素出棧pop、pop2
  • 控制轉(zhuǎn)移指令:可以讓JVM有條件或無條件地從指定位置指令繼續(xù)執(zhí)行程序,如條件分支if系列;如無條件分支goto
  • 方法調(diào)用和返回指令:如invokevirtural 用于調(diào)用對(duì)象的實(shí)例方法、Treturn 返回值
  • 異常處理指令:throw語句拋出的異常,由athrow指令來實(shí)現(xiàn)
  • 同步指令:處理同步操作,如synchronized關(guān)鍵字由 monitorenter 和 monitorexit 指令來實(shí)現(xiàn)

棧幀基礎(chǔ)

每執(zhí)行調(diào)用一個(gè)方法,將用一個(gè)棧幀來支持此方法的執(zhí)行。棧幀中存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等。每一個(gè)方法調(diào)用開始至執(zhí)行完成的過程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過程。

在進(jìn)行字節(jié)碼指令操作時(shí),需要確定數(shù)據(jù)歸屬到什么變量,需要局部變量表;需要對(duì)數(shù)據(jù)進(jìn)行操作并存取,需要操作數(shù)棧;需要知道執(zhí)行到哪,需要程序計(jì)數(shù)器;可能需要在運(yùn)行時(shí)轉(zhuǎn)化調(diào)用的具體方法,需要?jiǎng)討B(tài)連接。

一個(gè)線程中的方法調(diào)用鏈可能會(huì)很長(zhǎng),很多方法同時(shí)處于執(zhí)行狀態(tài),對(duì)于執(zhí)行引擎來說,在活動(dòng)線程中,只有位于棧頂?shù)臈攀怯行У?,稱為當(dāng)前棧幀(Current Stack Frame),與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作。

棧幀結(jié)構(gòu)如下圖


棧幀結(jié)構(gòu)圖.jpg

圖片來自 《深入理解Java虛擬機(jī)》

局部變量表

局部變量表用來存放方法參數(shù)和方法內(nèi)部定義的局部變量,最大所需容量有max_locals表示,單位為Slot。

JVM沒有指明一個(gè) Slot占用的內(nèi)存空間大小。Slot可以用32位或更小的物理內(nèi)存來存放,也可以在64位虛擬機(jī)中使用64位的物理內(nèi)存去實(shí)現(xiàn)一個(gè)Slot。對(duì)于64位的數(shù)據(jù)類型,虛擬機(jī)會(huì)以高位對(duì)齊的方式分配兩個(gè)連續(xù)的Slot空間。Slot除了能存放基礎(chǔ)數(shù)據(jù)類型外,還能存儲(chǔ)reference和returnAddress,reference為一個(gè)對(duì)象實(shí)例的引用,能通過此引用直接或間接地查找到對(duì)象在Java堆中的數(shù)據(jù)存放的起始地址索引,以及直接或間接地查找到對(duì)象所屬數(shù)據(jù)類型在方法區(qū)中的存儲(chǔ)的類型索引。

Slot是可以重用的,如果在之后的執(zhí)行區(qū)域里,局部變量如x不再使用,則x占用的Slot將會(huì)被清理再做他用。如果方法不是靜態(tài)方法,一般第0個(gè)Slot為 “this”。

操作數(shù)棧

字節(jié)碼指令進(jìn)行操作時(shí),將從操作數(shù)棧中寫入和提取內(nèi)容。操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配。任意時(shí)刻不會(huì)超過max_stacks。

動(dòng)態(tài)連接

每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中(位于方法區(qū)),該棧幀所屬方法的引用,以支持方法調(diào)用過程中的動(dòng)態(tài)連接。在源文件被編譯成.Class文件后,.Class文件中常量池存有大量的符號(hào)引用。字節(jié)碼指令進(jìn)行方法調(diào)用時(shí),會(huì)以指向方法的符號(hào)引用作為參數(shù)。這些符號(hào)引用一部分在類加載或第一次使用時(shí)轉(zhuǎn)化為直接引用,稱為靜態(tài)解析。而另一部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,稱為動(dòng)態(tài)連接。

方法返回地址

一個(gè)方法執(zhí)行后有兩種方式退出。

  • 正常退出:遇到任意代表方法返回的字節(jié)碼指令,將返回值返回給上層調(diào)用者
  • 異常退出:執(zhí)行過程遇到了異常,并且沒用在方法內(nèi)進(jìn)行處理,沒有返回值。

不管哪一種方法退出,都需要返回到方法被調(diào)用的位置,讓程序繼續(xù)執(zhí)行。返回地址位置,可以通過程序計(jì)數(shù)器來確定,將程序計(jì)數(shù)器的值指向下一條執(zhí)行,令程序繼續(xù)執(zhí)行。

字節(jié)碼運(yùn)行

說了這么多,通過幾個(gè)例子看字節(jié)碼如何運(yùn)行。

簡(jiǎn)單的運(yùn)算

    public static int addAndDouble(int a, int b){
        return (a + b) * 2;
    }

函數(shù)將a和b相加然后乘以2,拿到結(jié)果返回。通過命令

javac fileName.java

編譯出.Class文件,能拿到具體的字節(jié)碼。
通過命令

javap -verbose class文件
能對(duì).Class進(jìn)行分析。

上面代碼轉(zhuǎn)成的字節(jié)碼以及字節(jié)碼指令為:


addAndDouble字節(jié)碼指令.jpg

addAndDouble()需要的操作數(shù)棧深為stack=2,局部變量表深度為locals=2,參數(shù)args_size=2個(gè)(因?yàn)槭莝tatic,不包含this),字節(jié)碼指令流為 1A 1B 60 06 68 AC 。字節(jié)碼命令前0、1、2等代表的是在指令在指令流中開始的位置。

0: iload_0   // 將第一個(gè)局部變量壓入棧,也就是代碼里的a
1: iload_1   // 將第二個(gè)局部變量壓入棧,也就是代碼里的b
2: iadd      // 將棧頂?shù)膬蓚€(gè)元素取出相加并入棧, 即a+b,暫用c表示
3: iconst_2  // 將常數(shù)2壓入棧
4: imul      // 將棧頂?shù)膬蓚€(gè)元素取出相乘并入棧,即 c * 2 ,暫用d表示
5: ireturn   //  將棧頂元素即d返回

過程用下圖表示


相加再乘2代碼字節(jié)碼運(yùn)行實(shí)例.png

同步方法和條件語句

代碼為

    public void syncFunction(int a){
        if (a == 2){
            synchronized (this){
                a++;
            }
        }
    }

代碼目的僅是為看同步操作和條件語句如何執(zhí)行,轉(zhuǎn)出的字節(jié)碼指令流和字節(jié)碼指令為:


條件判斷和同步.jpg

syncFunction()是有兩個(gè)參數(shù)的,第一個(gè)為this,第二個(gè)則為傳來的a,對(duì)于操作數(shù)棧和局部變量表的操作與之前沒有區(qū)別,只需注意在有this時(shí)存于局部變量表第一位。astore命令時(shí)從操作數(shù)棧取出數(shù)據(jù)存入局部變量。

”2:“ 為代碼if翻譯出的字節(jié)碼指令,當(dāng)滿足條件時(shí)從"5:"處繼續(xù)執(zhí)行指令,如果不滿足條件則跳轉(zhuǎn)到 "22:" 處。字節(jié)碼指令是可以帶參數(shù)的,“2:”的下一條指令從"5:"開始,if翻譯出的指令占了三個(gè)字節(jié),為 A0 00 14,其中A0代表if_icmpne指令,0x0014 為參數(shù),十進(jìn)制值為20,即要跳轉(zhuǎn)的指令位置,當(dāng)不滿足if條件時(shí)跳轉(zhuǎn)到字節(jié)碼指令流第“2 + 20”處的指令,也就是“22:”處的指令。因?yàn)榉椒ㄕ加玫淖畲笞止?jié)為65535,因此用兩位字節(jié)表示跳轉(zhuǎn)位置足夠。

“8:” ~ “13:” 是同步代碼里的正常運(yùn)行指令,簡(jiǎn)單了解即可。

異常調(diào)用

代碼為

    public void exceptionFunction(){
        try {
            File file = new File("");
            file.getName();
        } catch (Exception e){
            e.printStackTrace();
        }
    }

轉(zhuǎn)出的字節(jié)碼指令流和字節(jié)碼指令為:


異常方法字節(jié)碼示例.jpg

如果沒有異常發(fā)生,執(zhí)行到“15:”的字節(jié)碼指令后,會(huì)跳轉(zhuǎn)到“23:”,意味著“18:” ~ "20:" 表示catch部分的執(zhí)行。代碼實(shí)例中,執(zhí)行“11:”會(huì)發(fā)生異常,進(jìn)入catch部分。

字節(jié)碼執(zhí)行小結(jié)

以上三個(gè)例子拋磚引玉說明字節(jié)碼指令的執(zhí)行,其它情況可以用類似方法分析。只要弄明白棧幀里局部變量表、操作數(shù)棧、返回地址、程序計(jì)數(shù)器的作用,以及字節(jié)碼指令含義和攜帶參數(shù)含義,就可以知道字節(jié)碼是怎樣執(zhí)行的。比如上一張圖片中, “6:”出invokespecial命令,攜帶一個(gè)參數(shù),參數(shù)指向的是常量池中的一個(gè)方法描述符,通過這些信息可以知道此命令是調(diào)用File的初始化函數(shù)創(chuàng)建對(duì)象。

字節(jié)碼指令的執(zhí)行可以簡(jiǎn)述為:

  • 運(yùn)算中需要的額外數(shù)據(jù)存儲(chǔ),需要局部變量表,
  • 對(duì)操作數(shù)進(jìn)行運(yùn)算,需要操作數(shù)棧
  • 程序計(jì)數(shù)器記錄指令執(zhí)行到哪

方法調(diào)用

上面部分說明了方法是怎樣執(zhí)行的,在執(zhí)行之前,JVM需知道具體要調(diào)用哪個(gè)方法,可以通過解析和分派完成。

解析

.Class文件
中存儲(chǔ)的都是符號(hào)引用,不是直接引用。因而在類加載的解析階段,會(huì)將一部分符號(hào)引用轉(zhuǎn)化為直接引用,前提是在程序真正運(yùn)行之前就有一個(gè)可以確定的調(diào)用版本,這個(gè)過程稱為 “解析” 。

執(zhí)行方法調(diào)用的字節(jié)碼指令有:

  • invokestatic: 調(diào)用靜態(tài)方法
  • invokespecial: 調(diào)用實(shí)例構(gòu)造器<init>方法、私有方法和父類方法
  • invokevirtual: 調(diào)用所有的虛方法
  • invokeinterface: 調(diào)用接口方法,會(huì)在運(yùn)行時(shí)再確定一個(gè)實(shí)現(xiàn)次接口的對(duì)象
  • invokedynamic: 在運(yùn)行時(shí)才能確定調(diào)用的具體方法,由調(diào)用點(diǎn)限定符確定

能被invokestatic和invokespecial指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,有 靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類方法 以及被標(biāo)識(shí)為 final的虛方法,沒有任何手段可以覆蓋或隱藏以上方法。

也因此解析是靜態(tài)過程,在編譯期間就可以確定,在類裝載的解析階段能把涉及到的符號(hào)引用全部轉(zhuǎn)變?yōu)榭梢源_定的直接引用。

分派

分派可以分為靜態(tài)分派和動(dòng)態(tài)分派,可以通過“重載”和“重寫”一探究竟。無論如何,目的是看虛擬機(jī)如何選擇正確的目標(biāo)方法。

靜態(tài)分派

代碼例子

public class StaticDispatch {
    static class Parent{ }

    static class Child extends Parent{ }

    public void call(Parent parent){
        System.out.println("call parent");
    }

    public void call(Child child){
        System.out.println("call child");
    }

    public static void main(String[] args) {
        Parent parent = new Child();
        StaticDispatch dispatch = new StaticDispatch();
        dispatch.call(parent);
    }
}

實(shí)際會(huì)輸出 “call parent”。 對(duì)于 Parent parent = new Child() 來說,前面的Parent稱為靜態(tài)變量,后面的Child稱為實(shí)際變量。靜態(tài)變量和實(shí)際變量在程序中都可以發(fā)生一些變化,區(qū)別是靜態(tài)變量的變化僅僅發(fā)生在使用時(shí),變量本身的靜態(tài)類型不會(huì)改變,在編譯期可知。JVM在確定重載版本時(shí),是通過靜態(tài)變量作為依據(jù)的。


靜態(tài)分派.jpg

對(duì)StaticDispatch的call()方法選取重載版本翻譯成字節(jié)碼指令時(shí),選擇的是Parent的版本。

動(dòng)態(tài)分派

動(dòng)態(tài)分派則不同,需要確定運(yùn)行時(shí)確定的數(shù)據(jù)類型,否則就亂了套。
代碼如下:

public class DynamicDispatch {
    static class Parent{
        public void hello(){
            System.out.println("hello parent");
        }
    }

    static class Child extends Parent{
        @Override
        public void hello(){
            System.out.println("hello child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Child();
        parent.hello();
    }
}

執(zhí)行結(jié)果輸出為 “hello child”,下圖為字節(jié)碼解析圖:


動(dòng)態(tài)分派.jpg

字節(jié)碼指令翻譯出來的靜態(tài)類型為Parent,但在方法執(zhí)行是,調(diào)用的是實(shí)際類型為Child的方法。在"8: " 處,將代碼新建的parent變量壓入棧,然后“9: ”處調(diào)用invokevirtual指令,調(diào)用的hello()方法歸屬于parent(實(shí)際類型為Child),parent也稱為方法的接收者(Receiver)。

invokevirtual指令的運(yùn)行過程分為以下步驟:

  1. 找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類型,記作C
  2. 如果在類型C中找到與常量中的描述符和簡(jiǎn)單名稱都相符的方法(也就是上圖紅圈右邊部分),則進(jìn)行權(quán)限校驗(yàn),通過則返回這個(gè)方法的直接引用,結(jié)束查找;否則,返回java.lang.IllegalAccessError異常
  3. 否則按照繼承關(guān)系從上往下按照步驟2查找
  4. 如果都沒有找到合適的方法,拋出java.lang.AbstracMethodError異常

因此,上面代碼的hello()方法的實(shí)際接收者類型為Child。invokevirtual指令在運(yùn)行期確定接收者的實(shí)際類型,是將常量池中的方法描述符指向了實(shí)際的直接引用上,這個(gè)過程是重寫的本質(zhì)。運(yùn)行期根據(jù)實(shí)際類型確定方法執(zhí)行版本的過程就是動(dòng)態(tài)分派。

多分派與單分派

分派除了能以靜態(tài)分派與動(dòng)態(tài)分派區(qū)分外,還能以多分派和單分派區(qū)分,兩者的區(qū)別在于選取方法是,參照的“宗量”,按照一個(gè)宗量選取的稱為單分派,按照多個(gè)宗量選取的稱為多分派。方法的接受者、方法的參數(shù)稱為方法的宗量,也可以不貼切地理解為依據(jù)。

public class MultiDispatch {

    static class Cigarette{} // 煙
    static class Toy{} // 玩具

    static class Parent{
        public void choice(Cigarette cigarette){
            System.out.println("parent choice cigarette");
        }
        public void choice(Toy toy){
            System.out.println("parent choice toy");
        }
    }

    static class Child extends Parent{
        public void choice(Cigarette cigarette){
            System.out.println("Child choice cigarette");
        }
        public void choice(Toy toy){
            System.out.println("Child choice Toy");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        parent.choice(new Cigarette());
        Parent child = new Child();
        child.choice(new Toy());
    }
}

運(yùn)行結(jié)果為

parent choice cigarette
Child choice Toy

多分派.jpg

在編譯時(shí)期,也就是靜態(tài)分派時(shí)期。選擇目標(biāo)方法的依據(jù)有亮點(diǎn):靜態(tài)類型、方法參數(shù)。因此翻譯成的字節(jié)碼指令invokevirtual的指令參數(shù)均指向了 Parent.choice(),一個(gè)指向的是常量符號(hào)引用是 Parent.choice(Cigarette),另一個(gè)是Parent.choice(Toy)。根據(jù)兩個(gè)宗量進(jìn)行原則,因此Java里的靜態(tài)分派數(shù)據(jù)多分派。

在運(yùn)行時(shí)期,也就是動(dòng)態(tài)分派過程。執(zhí)行choice()方法時(shí),需要確定接收者的實(shí)際類型,因?yàn)橐獔?zhí)行的方法已被確認(rèn),無需關(guān)心,因此參數(shù)的靜態(tài)類型、實(shí)際類型都不會(huì)對(duì)方法的選擇構(gòu)成影響。只有接收者的實(shí)際類型會(huì)構(gòu)成影響。因此動(dòng)態(tài)分派屬于單分派類型,只以一個(gè)宗量作為選擇。

總結(jié)

JVM 方法的執(zhí)行可以總結(jié)為以下幾點(diǎn):

  1. 方法調(diào)用時(shí),根據(jù)字節(jié)碼指令的不同,以解析和分派確定目標(biāo)方法
  2. invokestatic、invokespecial指令和以final聲明的方法可以以解析確認(rèn)目標(biāo)方法,invokevirtual、invokeinterface、invokedynamic則以分派方式確認(rèn)目標(biāo)方法
  3. 分派需要考慮的宗量為:接收者的靜態(tài)類型和實(shí)際類型,參數(shù)的靜態(tài)類型。靜態(tài)分派時(shí)考慮接受者的靜態(tài)類型和參數(shù)的靜態(tài)類型;動(dòng)態(tài)分派考慮接受者的實(shí)際類型
  4. 方法代碼最終轉(zhuǎn)換為緊湊的字節(jié)碼指令流,不同的字節(jié)碼值代表不同的指令,后面可能會(huì)帶有0到多個(gè)參數(shù),查表可以知道對(duì)象的字節(jié)碼含義以及參數(shù)含義,進(jìn)而進(jìn)行指令操作
  5. 字節(jié)碼指令執(zhí)行時(shí),需要程序計(jì)數(shù)器記錄執(zhí)行到的指令,以能執(zhí)行下一條指令;執(zhí)行過程需要存儲(chǔ)數(shù)據(jù),借助局部變量表存儲(chǔ);指令執(zhí)行時(shí)需要對(duì)操作數(shù)進(jìn)行運(yùn)算,通過操作數(shù)棧進(jìn)行存取
  6. return系列指令代表一個(gè)方法正常執(zhí)行結(jié)束,棧幀出棧,下一棧幀,也就是方法的調(diào)用這個(gè)方法的方法繼續(xù)從它的程序計(jì)數(shù)器指向的下一條指令繼續(xù)執(zhí)行

參考

《深入理解 Java 虛擬機(jī)》—— 第 8 章
Java字節(jié)碼分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容