1. 程式人生 > 其它 >12 虛擬機器位元組碼執行引擎_位元組碼執行機制

12 虛擬機器位元組碼執行引擎_位元組碼執行機制

目錄

1 解釋執行與編譯執行

  1. 解釋執行:通過直譯器執行
  2. 編譯執行:通過即時編譯器產生原生代碼(二進位制程式碼)執行
  3. 虛擬機器的執行引擎支援以上兩種方式

java語言的編譯及執行過程:

C語言的編譯過程:

2 基於棧/暫存器的指令集

  1. 基於棧的指令集架構:指令不帶引數,使用運算元棧中的資料作為指令的運算輸入,指令的運算結果也儲存在運算元棧中。依賴運算元棧進行工作
  2. 基於暫存器的指令集架構:X-86指令集,指令帶有運算元依賴暫存器計算和儲存。
  3. 位元組碼指令屬於基於棧的指令集架構(部分指令會帶有引數)

溫馨提示:關於x86指令集在《深入理解計算機作業系統》讀書筆記系列中有詳細介紹

以計算1+1作為例子,體現兩種指令集的差異

基於棧的指令集:

iconst_1  
iconst_1 
iadd 
istore_0
  1. 兩條iconst_1指令連續把兩個常量1壓入棧
  2. iadd指令把棧頂的兩個值出棧、相加,然後把結果放回棧頂
  3. 最後istore_0把棧頂的值放到區域性變量表的第0個變數槽中

基於暫存器的指令集:

mov eax, 1 
add eax, 1
  1. mov指令將立即數1放入eax暫存器
  2. add指令將eax暫存器的值加1,並把結果儲存在eax暫存器中

棧的指令集的優缺點:

  1. 可移植,因為它不依賴暫存器(暫存器一般跟硬體體系掛鉤)
  2. 指令相對更加簡潔(暫存器指令集中還需要存放參數)
  3. 編譯器實現更加簡單(不需要考慮空間分配的問題,所需空間都在棧上操作)
  4. 缺點1:棧實現在記憶體中,比起暫存器的訪存速度更慢。
  5. 缺點2:指令數量比暫存器架構來得更多(出棧、入棧操作本身就產生了相當大量的指令)

3 直譯器執行位元組碼過程

例子說明Java虛擬機器如何執行位元組碼

public class ByteCodeTest_3 {
    public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }
}

javac編譯後的位元組碼完整內容如下:

minnesota-book:test howing.zhang$ javap -verbose ./ByteCodeTest_3.class
Classfile /Users/howing.zhang/hy-Data/Work/study/project/mconcurrency/src/main/java/com/minnesota/practice/test/ByteCodeTest_3.class
  Last modified 2022-12-6; size 310 bytes
  MD5 checksum 542ed788c7ea7a11f0c7cd0cf046c2a6
  Compiled from "ByteCodeTest_3.java"
public class com.minnesota.practice.test.ByteCodeTest_3
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // com/minnesota/practice/test/ByteCodeTest_3
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               calc
   #9 = Utf8               ()I
  #10 = Utf8               SourceFile
  #11 = Utf8               ByteCodeTest_3.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               com/minnesota/practice/test/ByteCodeTest_3
  #14 = Utf8               java/lang/Object
{
  public com.minnesota.practice.test.ByteCodeTest_3();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100
         2: istore_1
         3: sipush        200
         6: istore_2
         7: sipush        300
        10: istore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: iload_3
        15: imul
        16: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 7
        line 8: 11
}
SourceFile: "ByteCodeTest_3.java"

用圖例來說明執行calc()方法過程中,運算元棧和區域性變量表的變化情況:

由上圖可知,calc()方法執行的棧幀結構中:

  1. 運算元棧的最大深度為2
  2. 本地變量表的變數槽最大使用數量為4
  3. 方法引數的個數為1,它是隱藏引數this
  4. 驗證過程也滿足:“stack=2, locals=4, args_size=1”該位元組碼內容

4 虛擬機器進行資料運算的一般過程

java虛擬機器進行資料運算,沒有異常的情況下,遵從以下步驟:

  1. 先入棧:將常量或變數值寫入運算元棧,push指令集負責。
  2. 出棧:將運算元棧資料儲存到常量表,store指令集負責
  3. 入棧:資料從常量表複製運算元棧,為下一步的運算進行準備。load指令集負責
  4. 運算:對棧內(從棧頂開始尋找)資料進行運算,並把結果重新存入棧頂。運算指令集負責
  5. 返回:把計算結果(棧頂資料)返回給方法呼叫者。