學習導圖#
一.為什麼要學習字節碼執行引擎?#
代碼編譯的結果從本地機器碼轉變為字節碼,是存儲格式發展的一小步,卻是編程語言發展的一大步
首先,拋出靈魂三問:
- 虛擬機在執行代碼的時候,如何找到正確的方法呢?
- 如何執行方法內的字節碼呢?
- 執行代碼時涉及的內存結構有哪些呢?
如果你對上述問題理解得還不是特別透徹的話,可以看下這篇文章;如果理解了,你可以關閉網頁,打開遊戲放鬆了hhh
下面,筆者將帶你探究 JVM 核心的組成部分之一——執行引擎。
二.核心知識點歸納#
2.1 概述#
Q1:虛擬機與物理機的異同
- 相同點:都有代碼執行能力
- 不同點:
物理機的執行引擎是直接建立在處理器、硬件、指令集和操作系統層面上的虛擬機的執行引擎是由自定義的,可自行制定指令集與執行引擎的結構體系,且能夠執行不被硬件直接支持的指令集格式
Q2:有關 JVM 字節碼執行引擎的概念模型
- 外觀上:所有 JVM 的執行引擎都是一致的。輸入的是字節碼文件,處理的是字節碼解析的等效過程,輸出的是執行結果
- 從實現上,執行引擎有多種執行 Java 代碼的選擇
解釋執行:通過解釋器執行編譯執行:通過即時編譯器產生本地代碼執行兩者兼備,甚至還會包含幾個不同級別的編譯器執行引擎
2.2 運行時棧幀結構#
2.2.1 基本概念#
筆者之前在 一文洞悉 JVM 內存管理機制 中就談到過虛擬機棧,相信看過的讀者都有印象
- 棧幀:用於支持虛擬機進行方法調用和方法執行的數據結構,是虛擬機棧的棧元素
- 存儲內容:方法的局部變量表、操作數棧、動態連接、方法返回地址和一些額外的附加信息
- 每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程
- 一個棧幀需要分配多少內存在程序編譯期就已確定 ,而不會受到程序運行期變量數據的影響
- 對於執行引擎來說,只有位於棧頂的棧幀(當前棧幀)才是有效的,即所有字節碼指令只對當前棧幀進行操作,與當前棧幀相關聯的方法稱為當前方法
2.2.2 局部變量表#
- 定義:局部變量表是一組變量值存儲空間
- 作用:存放方法參數和方法內部定義的局部變量
- 分配時期:Java 程序編譯為 Class 文件時,會在方法的 Code 屬性的 max_locals 數據項中確定了該方法所需要分配的局部變量表的最大容量
- 最小單位:變量槽
大小:虛擬機規範中沒有明確指明一個變量槽佔用的內存空間大小,允許變量槽長度隨著處理器、操作系統或虛擬機的不同而發生變化對於 32 位以內的數據類型(boolean、byte、char、short、int、float、reference、returnAddress ),虛擬機會為其分配一個變量槽空間對於 64 位的數據類型(long、double ),虛擬機會以高位對齊的方式為其分配兩個連續的變量槽空間特點:可重用。為了儘可能節省棧幀空間,若當前字節碼 PC 計數器的值已超出了某個變量的作用域,則該變量對應的變量槽可
交給其他變量使用- 訪問方式:通過索引定位。索引值的範圍是從 0 開始至局部變量表最大的變量槽數量
- 局部變量表第一項是名為 this 的一個當前類引用,它指向堆中當前對象的引用(由反編譯得到的局部變量表可知)
2.2.3 操作數棧#
- 操作數棧是一個後入先出棧
- 作用:在方法執行過程中,寫入(進棧)和提取(出棧)各種字節碼指令
- 分配時期:同上,在編譯時會在方法的 Code 屬性的 max_stacks 數據項中確定操作數棧的最大深度
- 棧容量:操作數棧的每一個元素可以是任意的 Java 數據類型 ——32 位數據類型所佔的棧容量為 1,64 位數據類型所佔的棧容量為 2
- 注意:操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在 編譯時編譯器需要驗證一次、在類校驗階段的數據流分析中還要再次驗證
2.2.4 動態連接#
- 定義:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接
- 靜態解析和動態連接區別:
Class 文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用作為參數,這些符號引用:
一部分會在類加載階段或者第一次使用的時候就轉化為直接引用(靜態解析)另一部分會在每一次
運行期間轉化為直接引用(動態連接)2.2.5 方法返回地址#
- 方法退出的兩種方式:
正常退出:執行中遇到任意一個方法返回的字節碼指令異常退出:執行中遇到異常、且在本方法的異常表中沒有搜索到匹配的異常處理器區處理
- 作用:在方法返回時都可能在棧幀中保存一些信息,用於恢復上層方法調用者的執行狀態
正常退出時,調用者的 PC 計數器的值可以作為返回地址異常退出時,通過異常處理器表來確定返回地址
- 方法退出的執行操作:
恢復上層方法的局部變量表和操作數棧若有返回值把它壓入調用者棧幀的操作數棧中調整 PC 計數器的值以指向方法調用指令後面的一條指令等
在實際開發中,一般會把動態連接、方法返回地址與其他附加信息全部一起稱為棧幀信息
2.3 方法調用#
- 方法調用是最普遍且頻繁的操作
- 任務:確定被調用方法的版本,即調用哪一個方法,不涉及方法內部的具體運行過程
下面筆者將為大家詳細講解方法調用的類型
2.3.1 解析調用#
筆者之前在 一夜搞懂 | JVM 類加載機制中就談到過解析,感覺有點混淆的,可以回去看下
- 特點:是靜態過程在編譯期間就完全確定,在類裝載的解析階段就會把涉及的符號引用全部轉變為可確定的直接引用,而不會延遲到運行期再去完成,即 編譯期可知、運行期不變
- 適用對象:private 修飾的私有方法,類靜態方法,類實例構造器,父類方法
2.3.2 分派調用#
Q1:什麼是靜態類型?什麼是實際類型?
A1:這個用代碼來說比較簡便, Talk is cheap ! Show me the code !
<code>Copy//父類
public class Human {
}
/<code>
<code>Copy//子類
public class Man extends Human {
}
/<code>
<code>Copypublic class Main {
public static void main(String[] args) {
//這裡的 Human 是靜態類型,Man 是實際類型
Human man=new Man();
}
}
/<code>
1.靜態分派#
依賴靜態類型來定位方法的執行版本典型應用是方法重載發生在編譯階段,不由 JVM 來執行
單純說未免有些許抽象,所以特地用下面的 DEMO 來幫助瞭解
<code>Copypublic class Father {
}
public class Son extends Father {
}
public class Daughter extends Father {
}
/<code>
<code>Copypublic 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");
}
}
/<code>
<code>Copypublic static void main(String[] args){
Father son = new Son();
Father daughter = new Daughter();
Hello hello = new Hello();
hello.sayHello(son);
hello.sayHello(daughter);
}
/<code>
輸出結果如下:
hello , i am the father
hello , i am the father
我們的編譯器在生成字節碼指令的時候會根據變量的靜態類型選擇調用合適的方法。就我們上述的例子而言:
2.動態分派#
依賴動態類型來定位方法的執行版本
典型應用是方法重寫
發生在運行階段,由 JVM 來執行
單純說未免有些許抽象,所以特地用下面的 DEMO 來幫助瞭解
<code>Copypublic 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");
}
}
/<code>
<code>Copypublic static void main(String[] args){
Father son = new Son();
son.sayHello();
}
/<code>
輸出結果如下:
hello world ---- son
我們接著來看一下字節碼指令調用情況
疑惑來了,我們可以看到,JVM 選擇調用的是靜態類型的對應方法,但是為什麼最終的結果卻調用了是實際類型的對應方法呢?
當我們將要調用某個類型實例的具體方法時,會首先將當前實例壓入操作數棧,然後我們的 invokevirtual 指令需要完成以下幾個步驟才能實現對一個方法的調用:
因此,疑惑自然解決了
3.單分派#
- 含義:根據一個宗量對目標方法進行選擇(方法的接受者與方法的參數統稱為方法的宗量)
4.多分派#
- 含義:根據多於一個宗量對目標方法進行選擇
想了解 靜態多分派,動態單分派 的可以看下這篇文章:Java 中的靜態單多分派與動態單分派
三.碎碎念#
恭喜你!已經看完了前面的文章,相信你對JVM字節碼執行引擎已經有一定深度的瞭解!你可以稍微放鬆獎勵自己一下,可以睡一個美美的覺,明天起來繼續沖沖衝!!!
最後
多說一句,很多人學Python過程中會遇到各種煩惱問題,沒有人解答容易放棄。小編是一名python開發工程師,這裡有我自己整理了一套最新的python系統學習教程,包括從基礎的python腳本到web開發、爬蟲、數據分析、數據可視化、機器學習等。想要這些資料的可以關注小編,並在後臺私信小編:“01”即可領取。
閱讀更多 Python初學者入門 的文章