前言
本文已經(jīng)收錄到我的 Github 個人博客,歡迎大佬們光臨寒舍:
學(xué)習(xí)導(dǎo)圖

一.為什么要學(xué)習(xí)字節(jié)碼執(zhí)行引擎?
代碼編譯的結(jié)果從本地機器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲格式發(fā)展的一小步,卻是編程語言發(fā)展的一大步
首先,拋出靈魂三問:
- 虛擬機在執(zhí)行代碼的時候,如何找到正確的方法呢?
- 如何執(zhí)行方法內(nèi)的字節(jié)碼呢?
- 執(zhí)行代碼時涉及的內(nèi)存結(jié)構(gòu)有哪些呢?
如果你對上述問題理解得還不是特別透徹的話,可以看下這篇文章;如果理解了,你可以關(guān)閉網(wǎng)頁,打開游戲放松了hhh
下面,筆者將帶你探究 JVM 核心的組成部分之一——執(zhí)行引擎。
二.核心知識點歸納
2.1 概述
Q1:虛擬機與物理機的異同
- 相同點:都有代碼執(zhí)行能力
- 不同點:
- 物理機的執(zhí)行引擎是直接建立在處理器、硬件、指令集和操作系統(tǒng)層面上的
- 虛擬機的執(zhí)行引擎是由自定義的,可自行制定指令集與執(zhí)行引擎的結(jié)構(gòu)體系,且能夠執(zhí)行不被硬件直接支持的指令集格式
Q2:有關(guān) JVM 字節(jié)碼執(zhí)行引擎的概念模型
- 外觀上:所有
JVM的執(zhí)行引擎都是一致的。輸入的是字節(jié)碼文件,處理的是字節(jié)碼解析的等效過程,輸出的是執(zhí)行結(jié)果

- 從實現(xiàn)上,執(zhí)行引擎有多種執(zhí)行
Java代碼的選擇
- 解釋執(zhí)行:通過解釋器執(zhí)行
- 編譯執(zhí)行:通過即時編譯器產(chǎn)生本地代碼執(zhí)行
- 兩者兼?zhèn)?,甚至還會包含幾個不同級別的編譯器執(zhí)行引擎
2.2 運行時棧幀結(jié)構(gòu)
2.2.1 基本概念
筆者之前在 一文洞悉 JVM 內(nèi)存管理機制 中就談到過虛擬機棧,相信看過的讀者都有印象
- 棧幀:用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),是虛擬機棧的棧元素
- 存儲內(nèi)容:方法的局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址和一些額外的附加信息
- 每一個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程
- 一個棧幀需要分配多少內(nèi)存在程序編譯期就已確定,而不會受到程序運行期變量數(shù)據(jù)的影響
- 對于執(zhí)行引擎來說,只有位于棧頂?shù)臈?strong>當(dāng)前棧幀)才是有效的,即所有字節(jié)碼指令只對當(dāng)前棧幀進行操作,與當(dāng)前棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法

2.2.2 局部變量表
- 定義:局部變量表是一組變量值存儲空間
- 作用:存放方法參數(shù)和方法內(nèi)部定義的局部變量
- 分配時期:
Java程序編譯為Class文件時,會在方法的Code屬性的max_locals數(shù)據(jù)項中確定了該方法所需要分配的局部變量表的最大容量 - 最小單位:變量槽
- 大?。禾摂M機規(guī)范中沒有明確指明一個變量槽占用的內(nèi)存空間大小,允許變量槽長度隨著處理器、操作系統(tǒng)或虛擬機的不同而發(fā)生變化
- 對于
32位以內(nèi)的數(shù)據(jù)類型(boolean、byte、char、short、int、float、reference、returnAddress),虛擬機會為其分配一個變量槽空間- 對于
64位的數(shù)據(jù)類型(long、double),虛擬機會以高位對齊的方式為其分配兩個連續(xù)的變量槽空間- 特點:可重用。為了盡可能節(jié)省棧幀空間,若當(dāng)前字節(jié)碼
PC計數(shù)器的值已超出了某個變量的作用域,則該變量對應(yīng)的變量槽可交給其他變量使用
訪問方式:通過索引定位。索引值的范圍是從 0 開始至局部變量表最大的變量槽數(shù)量
-
局部變量表第一項是名為
this的一個當(dāng)前類引用,它指向堆中當(dāng)前對象的引用(由反編譯得到的局部變量表可知)局部變量表
2.2.3 操作數(shù)棧
操作數(shù)棧是一個后入先出棧
作用:在方法執(zhí)行過程中,寫入(進棧)和提?。ǔ鰲#└鞣N字節(jié)碼指令
分配時期:同上,在編譯時會在方法的
Code屬性的max_stacks數(shù)據(jù)項中確定操作數(shù)棧的最大深度棧容量:操作數(shù)棧的每一個元素可以是任意的
Java數(shù)據(jù)類型 ——32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2注意:操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴格匹配,在編譯時編譯器需要驗證一次、在類校驗階段的數(shù)據(jù)流分析中還要再次驗證
2.2.4 動態(tài)連接
- 定義:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接
- 靜態(tài)解析和動態(tài)連接區(qū)別:
Class文件的常量池中存有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用作為參數(shù),這些符號引用:
- 一部分會在類加載階段或者第一次使用的時候就轉(zhuǎn)化為直接引用(靜態(tài)解析)
- 另一部分會在每一次運行期間轉(zhuǎn)化為直接引用(動態(tài)連接)
2.2.5 方法返回地址
- 方法退出的兩種方式:
- 正常退出:執(zhí)行中遇到任意一個方法返回的字節(jié)碼指令
- 異常退出:執(zhí)行中遇到異常、且在本方法的異常表中沒有搜索到匹配的異常處理器區(qū)處理
- 作用:在方法返回時都可能在棧幀中保存一些信息,用于恢復(fù)上層方法調(diào)用者的執(zhí)行狀態(tài)
- 正常退出時,調(diào)用者的
PC計數(shù)器的值可以作為返回地址- 異常退出時,通過異常處理器表來確定返回地址
- 方法退出的執(zhí)行操作:
- 恢復(fù)上層方法的局部變量表和操作數(shù)棧
- 若有返回值把它壓入調(diào)用者棧幀的操作數(shù)棧中
- 調(diào)整
PC計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等
在實際開發(fā)中,一般會把動態(tài)連接、方法返回地址與其他附加信息全部一起稱為棧幀信息
2.3 方法調(diào)用
- 方法調(diào)用是最普遍且頻繁的操作
- 任務(wù):確定被調(diào)用方法的版本,即調(diào)用哪一個方法,不涉及方法內(nèi)部的具體運行過程
下面筆者將為大家詳細講解方法調(diào)用的類型
2.3.1 解析調(diào)用
筆者之前在 一夜搞懂 | JVM 類加載機制中就談到過解析,感覺有點混淆的,可以回去看下
- 特點:
- 是靜態(tài)過程
- 在編譯期間就完全確定,在類裝載的解析階段就會把涉及的符號引用全部轉(zhuǎn)變?yōu)榭纱_定的直接引用,而不會延遲到運行期再去完成,即編譯期可知、運行期不變
- 適用對象:
private修飾的私有方法,類靜態(tài)方法,類實例構(gòu)造器,父類方法
2.3.2 分派調(diào)用
Q1:什么是靜態(tài)類型?什么是實際類型?
A1:這個用代碼來說比較簡便, Talk is cheap ! Show me the code !
//父類
public class Human {
}
//子類
public class Man extends Human {
}
public class Main {
public static void main(String[] args) {
//這里的 Human 是靜態(tài)類型,Man 是實際類型
Human man=new Man();
}
}
1.靜態(tài)分派
- 依賴靜態(tài)類型來定位方法的執(zhí)行版本
- 典型應(yīng)用是方法重載
- 發(fā)生在編譯階段,不由
JVM來執(zhí)行單純說未免有些許抽象,所以特地用下面的
DEMO來幫助了解
public class Father {
}
public class Son extends Father {
}
public class Daughter extends Father {
}
public class Hello {
public void sayHello(Father father){
System.out.println("hello , i am the father");
}
public void sayHello(Daughter daughter){
System.out.println("hello i am the daughter");
}
public void sayHello(Son son){
System.out.println("hello i am the son");
}
}
public static void main(String[] args){
Father son = new Son();
Father daughter = new Daughter();
Hello hello = new Hello();
hello.sayHello(son);
hello.sayHello(daughter);
}
輸出結(jié)果如下:
hello , i am the father
hello , i am the father
我們的編譯器在生成字節(jié)碼指令的時候會根據(jù)變量的靜態(tài)類型選擇調(diào)用合適的方法。就我們上述的例子而言:

2.動態(tài)分派
依賴動態(tài)類型來定位方法的執(zhí)行版本
典型應(yīng)用是方法重寫
發(fā)生在運行階段,由
JVM來執(zhí)行單純說未免有些許抽象,所以特地用下面的
DEMO來幫助了解
public class Father {
public void sayHello(){
System.out.println("hello world ---- father");
}
}
//繼承 + 方法重寫
public class Son extends Father {
@Override
public void sayHello(){
System.out.println("hello world ---- son");
}
}
public static void main(String[] args){
Father son = new Son();
son.sayHello();
}
輸出結(jié)果如下:
hello world ---- son
我們接著來看一下字節(jié)碼指令調(diào)用情況


疑惑來了,我們可以看到,
JVM選擇調(diào)用的是靜態(tài)類型的對應(yīng)方法,但是為什么最終的結(jié)果卻調(diào)用了是實際類型的對應(yīng)方法呢?
當(dāng)我們將要調(diào)用某個類型實例的具體方法時,會首先將當(dāng)前實例壓入操作數(shù)棧,然后我們的 invokevirtual 指令需要完成以下幾個步驟才能實現(xiàn)對一個方法的調(diào)用:

因此,疑惑釋然

3.單分派
- 含義:根據(jù)一個宗量對目標方法進行選擇(方法的接受者與方法的參數(shù)統(tǒng)稱為方法的宗量)
4.多分派
- 含義:根據(jù)多于一個宗量對目標方法進行選擇
想了解 靜態(tài)多分派,動態(tài)單分派 的可以看下這篇文章:Java 中的靜態(tài)單多分派與動態(tài)單分派
三.碎碎念
恭喜你!已經(jīng)看完了前面的文章,相信你對
JVM字節(jié)碼執(zhí)行引擎已經(jīng)有一定深度的了解!你可以稍微放松獎勵自己一下,可以睡一個美美的覺,明天起來繼續(xù)沖沖沖!??!

如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力
本文參考鏈接:
