JVM學習(五) -執行子系統
- 2020 年 11 月 9 日
- 筆記
虛擬機和物理機的區別。兩種都有程式碼執行能力。物理機的執行引擎是建立在處理器、硬體、指令集和作業系統上。而虛擬機的執行引擎是有自己實現的。因此可以自行的制定指令集和執行引擎的結構關係。
個人理解:分為三個部分。分別是介紹運行時分的各個記憶體區域【程式計數器、java虛擬機棧【局部變數表、操作數棧、動態鏈接、方法返回地址】、本地方法棧、方法區、堆】。方法的調用【解析、分派】、方法的執行【解析執行、編譯執行】
1. 運行時棧幀結構
棧幀是用於虛擬機進行方法調用和方法執行的數據結構。是虛擬機棧的棧元素。這部分內容和運行記憶體區域中JAVA虛擬機棧中的棧幀結構部分有重疊的部分。一個棧幀包括局部變數表、操作數棧、動態鏈接和方法返回地址四部分。棧幀需要多大的局部變數表和多深的操作數棧在編譯階段都已經確定的。
Java虛擬機的解釋執行引擎稱為「基於棧的執行引擎」,其中的棧指的是操作數棧。
程式計數器、虛擬機棧、本地方法棧都是執行緒級別的。跟著現成的生而生、跟著執行緒的滅而滅。意味著,每個執行緒都有自己的java虛擬機棧。該執行緒中調用的方法都作為一個棧幀,壓棧和彈棧。
(1) 局部變數表
主要用來存儲方法的參數和方法內部定義的的局部變數。局部變數表是一組值存儲空間。
一個方法只有在調用完成後才會釋放。因而,如果一個方法之前定義了大記憶體的對象,但是,在方法後半部分已經不需要這部分記憶體了。同時,後半部分需要執行很長時間。這種情況下,會造成前半部分創建的大容量的對象既不被使用需要,又因為方法因為沒有執行完成而釋放。這部分記憶體會一致占著不能被釋放。所以,也可以在方法內,將不需要繼續使用的變數賦值為null。可以讓垃圾回收器及時的回收。
(2) 操作數棧
操作數棧的深度在編譯的時候已經確定。在方法執行的過程中,從局部變數表中獲取數據,壓棧。需要計算的時候根據運算符號從棧頂彈棧獲得數據進行計算,將結果壓棧。
(3) 動態鏈接
在class文件中,一個方法需要調用其它方法。需要將這些方法的符號引用轉換成記憶體地址中的直接引用。而這些符號引用存在方法區的常量池中。
每個棧的棧幀都包含一個指向運行時常量池中該方法的符號引用。持有這個引用的目的是支援方法調用中的動態鏈接。
這些符號引用一部分會在類載入階段或者第一次使用的時候轉換成了直接引用。這種轉換稱為靜態解析。另一部分將在每一次運行期間轉化成直接引用。這部分稱為動態鏈接。
(4) 方法返回地址
當一個方法開始執行時,可能有兩種方式退出該方法。正常完成出口和異常完成出口。
無論方法採用何種方式退出,在方法退出後都需要返回到方法被調用的位置,程式才能繼續執行,方法返回時可能需要在當前棧幀中保存一些資訊,用來幫他恢復它的上層方法執行狀態。
2. 方法調用
方法調用並不等同於方法執行,核心任務是:確定被調用方法的版本【即調用的是哪一個方法】。不涉及方法內部的具體執行運行過程。
所有方法之間的調用在class文件中存儲的是符號引用。而不是具體方法【實際運行時記憶體布局的入口地址】的直接引用。需要在類載入甚至在運行期間才能確定目標方法的直接引用。
(1) 解析
調用的目標方法都是方法區的常量池中的一個符號引用。如果程式在真正運行之前就可以確定方法調用的版本,並且這個方法調用的版本在運行期間是不會發生變化的。這種情境下,在類載入的解析階段,會將符號引用轉換成直接引用。這種方法的調用稱之為解析。
符合「編譯期可知,運行期不可變」的要求的方法,主要包括靜態方法和私有方法兩大類。前者與類直接關聯,後者外部不可被訪問。他們特點是不可能通過繼承或者其它方式重寫其它版本。因此,都會通過解析的方式進行方法調用。
解析對應5條方法調用指令:
① Invoke static :調用靜態方法
② Invoke special:調用實例構造器<init>方法,私有方法和父類方法。
③ Invoke virtual:調用所有的虛方法。【final修飾的方法,因為不能被覆蓋,沒有其它版本,所以,無需對方法進行多態選擇】
④ Invoke Interface:調用介面方法(多態)
⑤ Invoke dynamic:
解析是一個靜態的過程,在類載入的鏈接階段的解析階段中,就會把涉及的符號引全部轉換成直接引用。
(2) 分派
① 靜態分派
所有依賴靜態類型【參數的聲明類型】來定位方法版本的分派稱為靜態分派。
靜態分派最典型的是重載。在編譯階段,根據參數的靜態類型就可以確定執行的是哪個版本的方法。靜態分派發生在編譯階段,因此,靜態分配的動作不由虛擬機來執行。
② 動態分派
運行期間根據實際類型確定方法執行的版本的分派稱為動態分派。
動態分派最典型的就是重寫。
JVM的invoke virtual指令了,這個指令的解析過程有助於我們更深刻理解重寫的本質。該指令的具體解析過程如下
- 找到操作數棧棧頂的第一個元素所指向的對象的實際類型,記為C。【獲得實際對象的類型】。
- 如果在類型C中找到與常量中描述符和簡單名稱都相符的方法,則進行訪問許可權的校驗,如果通過則返回這個方法的直接引用,查找結束;如果不通過,則返回非法訪問異常【在C中找和當前方法匹配的方法】
- 如果在類型C中沒有找到,則按照繼承關係從下到上依次對C的各個父類進行第2步的搜索和驗證過程【首先在子類中找,沒有再在父類中找。和雙親委派相反,雙親委派優先在父類載入器中處理,而重載優先在子類中匹配】
- 如果始終沒有找到合適的方法,則拋出抽象方法錯誤的異常。
③ 單分派和多分派
(3) 動態類型語言支援
3. 執行引擎
核心理解虛擬機如果執行方法中的位元組碼指令集。其實就是基於棧的位元組碼解釋執行引擎做的事情。JAVA虛擬機執行程式碼的時候,都有解釋執行【通過解釋器執行】和編譯執行【通過編譯器產生本地程式碼執行】兩種選擇。
執行引擎:將位元組碼指令解析/編譯為對應平台上本地機器指令。簡而言之,執行引擎充當高級語言和機器碼之間的翻譯者。
其中,執行引擎中 解釋器(interpreter)提供解釋執行功能。JIT Completer 提供編譯執行的功能。

(1) 解釋執行
JVM得到位元組碼之後。通過解析器Interpreter解析成最終的機器碼。
(2) 基於棧的指令集和基於暫存器的指令集
基於棧的指令集:將指令集保存在棧中。大部分是零地址指令集,依賴操作數棧進行工作。Java編譯器輸出的指令集是基於棧的指令集。
PC電腦支援的就是基於暫存器的指令集。
(3) 基於棧的解釋器執行過程
從指令集順序執行,將局部變數經過操作數棧,最後在局部變數表中生成。運算的過程是從局部變數表獲得數據,然後放入操作數棧。從操作數棧彈出進行計算。之後將結果壓棧。
先將數放入操作數棧,然後將操作數棧中的數據彈出放入局部變數表中,這樣一步步將局部變數表的變數賦值。運算過程中,從局部變數表中獲取數據壓入操作數棧中。然後根據指令,從操作數棧中彈出數據進行計算,之後將計算結果壓棧。
(4) 編譯執行
JVM平台提供一種及時編譯技術,及時編譯的目的是避免函數被解釋執行。而是將整個函數體編譯成機器碼。每次函數執行時,直接執行編譯後的機器碼。這種方式能將效率大幅提升。

