本文已經收錄到個人 Github 我的博客,歡迎大佬們光臨寒舍:java
個人 GIthub 博客git
代碼編譯的結果從本地機器碼轉變爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步github
首先,拋出靈魂三問:編程
若是你對上述問題理解得還不是特別透徹的話,能夠看下這篇文章;若是理解了,你能夠關閉網頁,打開遊戲放鬆了hhh數據結構
下面,筆者將帶你探究 JVM
核心的組成部分之一——執行引擎。編程語言
Q1:虛擬機與物理機的異同ide
- 物理機的執行引擎是直接創建在處理器、硬件、指令集和操做系統層面上的
- 虛擬機的執行引擎是由自定義的,可自行制定指令集與執行引擎的結構體系,且可以執行不被硬件直接支持的指令集格式
Q2:有關 JVM
字節碼執行引擎的概念模型post
JVM
的執行引擎都是一致的。輸入的是字節碼文件,處理的是字節碼解析的等效過程,輸出的是執行結果Java
代碼的選擇
- 解釋執行:經過解釋器執行
- 編譯執行:經過即時編譯器產生本地代碼執行
- 二者兼備,甚至還會包含幾個不一樣級別的編譯器執行引擎
筆者以前在 一文洞悉 JVM 內存管理機制 中就談到過虛擬機棧,相信看過的讀者都有印象學習
Java
程序編譯爲 Class
文件時,會在方法的 Code
屬性的 max_locals
數據項中肯定了該方法所須要分配的局部變量表的最大容量
- 大小:虛擬機規範中沒有明確指明一個變量槽佔用的內存空間大小,容許變量槽長度隨着處理器、操做系統或虛擬機的不一樣而發生變化
- 對於
32
位之內的數據類型(boolean
、byte
、char
、short
、int
、float
、reference
、returnAddress
),虛擬機會爲其分配一個變量槽空間- 對於
64
位的數據類型(long
、double
),虛擬機會以高位對齊的方式爲其分配兩個連續的變量槽空間- 特色:可重用。爲了儘量節省棧幀空間,若當前字節碼
PC
計數器的值已超出了某個變量的做用域,則該變量對應的變量槽可交給其餘變量使用
訪問方式:經過索引定位。索引值的範圍是從 0 開始至局部變量表最大的變量槽數量this
局部變量表第一項是名爲 this
的一個當前類引用,它指向堆中當前對象的引用(由反編譯獲得的局部變量表可知)
操做數棧是一個後入先出棧
做用:在方法執行過程中,寫入(進棧)和提取(出棧)各類字節碼指令
分配時期:同上,在編譯時會在方法的 Code
屬性的 max_stacks
數據項中肯定操做數棧的最大深度
棧容量:操做數棧的每個元素能夠是任意的 Java
數據類型 ——32
位數據類型所佔的棧容量爲 1
,64
位數據類型所佔的棧容量爲 2
注意:操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯時編譯器須要驗證一次、在類校驗階段的數據流分析中還要再次驗證
Class
文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數,這些符號引用:
- 一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用(靜態解析)
- 另外一部分會在每一次運行期間轉化爲直接引用(動態鏈接)
- 正常退出:執行中遇到任意一個方法返回的字節碼指令
- 異常退出:執行中遇到異常、且在本方法的異常表中沒有搜索到匹配的異常處理器區處理
- 正常退出時,調用者的
PC
計數器的值能夠做爲返回地址- 異常退出時,經過異常處理器表來肯定返回地址
- 恢復上層方法的局部變量表和操做數棧
- 如有返回值把它壓入調用者棧幀的操做數棧中
- 調整
PC
計數器的值以指向方法調用指令後面的一條指令等
在實際開發中,通常會把動態鏈接、方法返回地址與其餘附加信息所有一塊兒稱爲棧幀信息
下面筆者將爲你們詳細講解方法調用的類型
筆者以前在 一晚上搞懂 | JVM 類加載機制中就談到過解析,感受有點混淆的,能夠回去看下
private
修飾的私有方法,類靜態方法,類實例構造器,父類方法Q1:什麼是靜態類型?什麼是實際類型?
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 是靜態類型,Man 是實際類型
Human man=new Man();
}
}
複製代碼
- 依賴靜態類型來定位方法的執行版本
- 典型應用是方法重載
- 發生在編譯階段,不禁
JVM
來執行單純說未免有些許抽象,因此特意用下面的
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);
}
複製代碼
輸出結果以下:
hello , i am the father
hello , i am the father
咱們的編譯器在生成字節碼指令的時候會根據變量的靜態類型選擇調用合適的方法。就咱們上述的例子而言:
依賴動態類型來定位方法的執行版本
典型應用是方法重寫
發生在運行階段,由
JVM
來執行單純說未免有些許抽象,因此特意用下面的
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();
}
複製代碼
輸出結果以下:
hello world ---- son
咱們接着來看一下字節碼指令調用狀況
疑惑來了,咱們能夠看到,
JVM
選擇調用的是靜態類型的對應方法,可是爲何最終的結果卻調用了是實際類型的對應方法呢?
當咱們將要調用某個類型實例的具體方法時,會首先將當前實例壓入操做數棧,而後咱們的 invokevirtual
指令須要完成如下幾個步驟才能實現對一個方法的調用:
所以,疑惑天然解決了
想了解 靜態多分派,動態單分派 的能夠看下這篇文章:Java 中的靜態單多分派與動態單分派
恭喜你!已經看完了前面的文章,相信你對
JVM
字節碼執行引擎已經有必定深度的瞭解!你能夠稍微放鬆獎勵本身一下,能夠睡一個美美的覺,明天起來繼續沖沖衝!!!
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: