前言

與上圖類似的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é)碼用途大致分為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)如下圖

局部變量表
局部變量表用來存放方法參數(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()需要的操作數(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返回
過程用下圖表示

同步方法和條件語句
代碼為
public void syncFunction(int a){
if (a == 2){
synchronized (this){
a++;
}
}
}
代碼目的僅是為看同步操作和條件語句如何執(zhí)行,轉(zhuǎn)出的字節(jié)碼指令流和字節(jié)碼指令為:

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é)碼指令為:

如果沒有異常發(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ù)的。

對(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é)碼解析圖:

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

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