要點提煉| 理解JVM之位元組碼執行引擎

釐米姑娘發表於2019-01-02

本篇將從概念模型的角度來介紹虛擬機器的方法呼叫和位元組碼執行。

  • 概述
  • 執行時棧幀結構
  • 方法呼叫

1.概述

a.有關虛擬機器與物理機的異同

  • 同:都有程式碼執行能力
  • 異:
    • 物理機的執行引擎是直接建立在處理器、硬體、指令集和作業系統層面上的
    • 虛擬機器的執行引擎是由自定義的,可自行制定指令集與執行引擎的結構體系,且能夠執行不被硬體直接支援的指令集格式

b.有關Java虛擬機器位元組碼執行引擎的概念模型

  • 從外觀上,所有Java虛擬機器的執行引擎都是一致的。輸入的是位元組碼檔案,處理的是位元組碼解析的等效過程,輸出的是執行結果
  • 從實現上,執行引擎有多種執行Java程式碼的選擇
    • 解釋執行:通過直譯器執行
    • 編譯執行:通過即時編譯器產生原生程式碼執行
    • 兩者兼備,甚至還會包含幾個不同級別的編譯器執行引擎

2.執行時棧幀結構

  • 棧幀(Stack Frame):用於支援虛擬機器進行方法呼叫和方法執行的資料結構,是虛擬機器執行時資料區中的虛擬機器棧的棧元素
  • 儲存內容:方法的區域性變數表、運算元棧、動態連線、方法返回地址一些額外的附加資訊
  • 每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。
  • 一個棧幀需要分配多少記憶體在程式編譯期就已確定,而不會受到程式執行期變數資料的影響
  • 對於執行引擎來說,只有位於棧頂的棧幀(當前棧幀)才是有效的,即所有位元組碼指令只對當前棧幀進行操作,與當前棧幀相關聯的方法稱為當前方法
  • 在概念模型上,典型的棧幀結構如圖:
    要點提煉| 理解JVM之位元組碼執行引擎

接下來詳細介紹棧幀中各個部分的作用和資料結構

a.區域性變數表

  • 區域性變數表(Local Variable Table)是一組變數值儲存空間
  • 作用:存放方法引數和方法內部定義的區域性變數
  • 分配時期:Java程式編譯為Class檔案時,會在方法的Code屬性的max_locals資料項中確定了該方法所需要分配的區域性變數表的最大容量
  • 最小單位:變數槽
    • 大小:虛擬機器規範中沒有明確指明一個變數槽佔用的記憶體空間大小,允許變數槽長度隨著處理器、作業系統或虛擬機器的不同而發生變化
      • 對於32位以內的資料型別(boolean、byte、char、short、int、float、reference、returnAddress ),虛擬機器會為其分配一個變數槽空間
      • 對於64位的資料型別(long、double ),虛擬機器會以高位對齊的方式為其分配兩個連續的變數槽空間
    • 特點:可重用。為了儘可能節省棧幀空間,若當前位元組碼PC計數器的值已超出了某個變數的作用域,則該變數對應的變數槽可交給其他變數使用
  • 訪問方式:通過索引定位。索引值的範圍是從0開始至區域性變數表最大的變數槽數量

b.運算元棧

  • 運算元棧(Operand Stack)是一個後入先出棧
  • 作用:在方法執行過程中,寫入(進棧)和提取(出棧)各種位元組碼指令
  • 分配時期:同樣的,在編譯時會在方法的Code屬性的max_locals資料項中確定運算元棧的最大深度

在方法執行的任何時候,運算元棧的深度都不會超過在max_stacks資料項中設定的最大值

  • 棧容量:運算元棧的每一個元素可以是任意的Java資料型別——32位資料型別所佔的棧容量為1,64位資料型別所佔的棧容量為2
  • 注意:運算元棧中元素的資料型別必須與位元組碼指令的序列嚴格匹配,在編譯時編譯器需要驗證一次、在類校驗階段的資料流分析中還要再次驗證

c.動態連線(Dynamic Linking):每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線

靜態解析和動態連線:Class檔案的常量池中存有大量的符號引用,位元組碼中的方法呼叫指令就以常量池中指向方法的符號引用作為引數,這些符號引用:

  • 一部分會在類載入階段或者第一次使用的時候就轉化為直接引用(靜態解析
  • 另一部分會在每一次執行期間轉化為直接引用(動態連線

d.方法返回地址

  • 方法退出的兩種方式:
    • 正常完成出口(Normal Method Invocation Completion):執行中遇到任意一個方法返回的位元組碼指令;可能會給上層呼叫者傳遞返回值
    • 異常完成出口(Abrupt Method Invocation Completion):執行中遇到異常、且在本方法的異常表中沒有搜尋到匹配的異常處理器區處理;不會給上層呼叫者傳遞返回值
  • 作用:無論哪種退出方式,在方法返回時都可能在棧幀中儲存一些資訊,用於恢復上層方法呼叫者的執行狀態
    • 正常退出時,呼叫者的PC計數器的值可以作為返回地址,棧幀中很可能會儲存這個計數器值
    • 異常退出時,通過異常處理器表來確定返回地址,棧幀中一般不會儲存這部分資訊
  • 方法退出的執行操作:恢復上層方法的區域性變數表和運算元棧、若有返回值把它壓入呼叫者棧幀的運算元棧中、調整PC計數器的值以指向方法呼叫指令後面的一條指令等

e.附加資訊:增加一些規範裡沒有描述的資訊到棧幀之中,如與除錯相關的資訊

在實際開發中,一般會把動態連線、方法返回地址與其他附加資訊全部一起稱為棧幀資訊


3.方法呼叫

  • 方法呼叫是最普遍、最頻繁的操作(方法呼叫≠方法執行)
  • 任務:確定被呼叫方法的版本,即呼叫哪一個方法,不涉及方法內部的具體執行過程
  • 型別:
    • 解析(Resolution)呼叫
      • 特點:是靜態過程;在編譯期間就完全確定,在類裝載的解析階段就會把涉及的符號引用全部轉變為可確定的直接引用,而不會延遲到執行期再去完成,即編譯期可知、執行期不可變
      • 適用物件:靜態方法(與型別直接關聯)、私有方法(在外部不可被訪問),它們都不可能通過繼承或其他方式重寫其他版本
    • 分派(Dispatch)呼叫
      • 特點:可靜可動
      • 型別:
        • 靜態分派:依賴靜態型別來定位方法的執行版本;典型應用是方法過載;發生在編譯階段,不由Java虛擬機器來執行
        • 動態分派:依賴動態型別來定位方法的執行版本;典型應用是方法重寫;發生在執行階段,由Java虛擬機器來執行
        • 單分派:根據一個宗量對目標方法進行選擇(方法的接受者與方法的引數統稱為方法的宗量)
        • 多分派:根據多於一個宗量對目標方法進行選擇

如何理解Java語言是一門靜態多分派、動態單分派的語言?


相關文章