如何構建你本身的 JVM (1) 解釋器

種一棵樹最好的時間是十年前, 其次是如今. php

0x00 幾點概念解釋器指令寄存器局部變量表操做數棧0x01 寄存器仍是棧場景寄存器方案棧方案混合方案, 結合寄存器和棧的方案(即 JVM 的方案)簡單比較0x02 解釋器核心0x03 簡單代碼實現寄存器混合關於上方代碼0x04 小結0x05 預告0x06 FAQjava

0x00 幾點概念

解釋器

解釋器, 是一種程序,可以把編程語言一行一行解釋運行。解釋器像是一位「中間人」,每次運行程序時都要先轉成另外一種語言再做運行,所以解釋器的程序運行速度比較緩慢。它不會一次把整個程序翻譯出來,而是每翻譯一行程序敘述就馬上運行,而後再翻譯下一行,再運行,如此不停地進行下去。c++

指令

一些相關的概念, 彙編指令, JVM 字節碼指令.
指令通常很簡單, 描述了一個具體的操做. 好比
彙編指令
mov &ex, 1 => 將整數 1 放到寄存器 ex 裏.
字節碼指令
bpush 1 => 將 byte 1 放到操做數棧頂.git

寄存器

簡單來講寄存器就是個 Map. 能夠根據寄存器地址(key)對其進行存取(value). 主要操做 get(key) , put(key,value)github

簡單來說, 後進先出, 只支持 push 和 pop 操做.
push : 將某個值放到棧的棧頂, 棧大小加 1.
pop : 將棧的棧頂的值弄出來, 棧大小減 1. web

局部變量表

棧幀內部的數據結構, 是個數組. 經過數組位置訪問, e.g array[0], array[1]. 換個角度來看, 其實能夠認爲是個特殊的寄存器. 只不過 key 是下標而已. 編程

操做數棧

棧幀內部數據結構, 同棧. 數組

0x01 寄存器仍是棧

脫離業務場景的技術設計都是耍流氓. -- 尼古拉斯.趙四bash

場景

僞代碼以下.數據結構

int a = 1 + 1; 
int b = 2 + 2;
int c = 0;
int d = b - a;
int d = d - c;
println(d)
複製代碼

若是正確運行, 必然輸出 2.
自動化的前提是能手動化. 因此人肉編譯一下吧.

寄存器方案

生成指令格式, inst [value|address]+
e.g

mov &0 1        => 把數值 1 放到寄存器 0 裏    
add &3 &0 &1 &2 => 把 寄存器 0 ,寄存器 1,  寄存器 2 裏的值相加, 並把結果放到 寄存器 3 裏.     
sub &3 &0 &1 &2 => 把 寄存器 0 裏的值 減去 寄存器 1,  寄存器 2 裏的值, 並把結果放到 寄存器 3 裏.     
println &3      => 取出 寄存器 3 的值 並輸出.     
複製代碼

生成的指令代碼以下. 爲方便理解, 雙斜槓以後爲註釋. 代表操做以後寄存器或棧的狀態

mov &0 1        // {0:1}
mov &1 1        // {0:1, 1:1}
add &2 &0 &1    // {0:1, 1:1, 2:2}

mov &3 2        // {0:1, 1:1, 2:2, 3:2}
mov &4 2        // {0:1, 1:1, 2:2, 3:2, 4:2}
add &5 &3 &4    // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4}

mov &6 0        // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0}

sub &7 &5 &2    // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0, 7:2}

sub &8 &7 &6    // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0, 7:2, 8:2} 

println &8      // {0:1, 1:1, 2:2, 3:2, 4:2, 5:4, 6:0, 7:2, 8:2} 
複製代碼

棧方案

生成指令格式 inst [value]{0,1}
e.g

push 1   => 將數值 1 推到棧頂. (..) -> (..,1)  
add      => 將棧頂的兩個數值相加, 並將結果放到棧頂. (..,v1,v2) -> (..,v1+v2)  
sub      => 假設棧頂值爲v2, (..,v1,v2) -> (..,v1-v2)  
swap     => 交換棧頂兩個元素 (v1,v2) -> (v2,v1)  
swap1    => 交換棧上第二,第三位置 (v1,v2,x) -> (v2,v1,x)  
println  => 棧頂數值出站, 並將結果輸出. (..,1) -> (..)  
複製代碼

生成的指令代碼以下. 爲方便理解, 雙斜槓以後爲註釋. 代表操做以後寄存器或棧的狀態

push 1  // (1)
push 1  // (1, 1)
add     // (2)

push 2  // (2, 2)
push 2  // (2, 2, 2)
add     // (2, 4)

push 0  // (2, 4, 0)

swap    // (2, 0, 4)
swap1   // (0, 2, 4)
swap    // (0, 4, 2)
sub     // (0, 2)

swap    // (2, 0)
sub     // (2)

println  // ()
複製代碼

混合方案, 結合寄存器和棧的方案(即 JVM 的方案)

e.g:

load   => 從寄存器中取值並 push 到操做數棧中.  
store  => 操做數棧頂數值出棧, 並存放到寄存器中.  
複製代碼

生成的指令代碼以下. 爲方便理解, 雙斜槓以後爲註釋. 代表操做以後寄存器或棧的狀態

push 1    // (1)     {}
push 1    // (1, 1)  {}
add       // (2)     {}
store &0  // ()      {0:2}

push 2    // (2)     {0:2}
push 2    // (2, 2)  {0:2}
add       // (4)     {0:2}
store &1  // ()      {0:2, 1:4}

push 0    // (0)     {0:2, 1:4}
store &2  // ()      {0:2, 1:4, 2:0}

load &1   // (4)     {0:2, 1:4, 2:0}
load &0   // (4, 2)  {0:2, 1:4, 2:0}
sub       // (2)     {0:2, 1:4, 2:0}
store &3  // ()      {0:2, 1:4, 2:0, 3:2}

load &3   // (2)     {0:2, 1:4, 2:2, 3:2}
load &2   // (2, 0)  {0:2, 1:4, 2:2, 3:2}
sub       // (2)     {0:2, 1:4, 2:2, 3:2}
store $4  // ()      {0:2, 1:4, 2:2, 3:2, 4:2}

load $4   // (2)     {0:2, 1:4, 2:2, 3:2, 4:2}
println   // ()      {0:2, 1:4, 2:2, 3:2, 4:2}
複製代碼

簡單比較

簡單場景下, 三種方案都可知足需求.
其中寄存器方案對應這計算機物理實現. CPU 的處理即是基於寄存器的. 優勢性能高, 數據的搬運次數少, 缺點指令複雜.
純粹基於棧的方案, 貌似沒有, 由於只有 push pop 兩種操做, 在局部變量較多的狀況下, 數據須要頻繁搬運. 優勢是指令簡單. 方便移植.
混合方案, 集兩家之長, 在移植性和效率上的折中方案.

0x02 解釋器核心

如上篇預告. 解釋器的核心是一個循環.

do {
    獲取下一個指令
    解釋指令
while (還有指令);
複製代碼

架構圖以下

mini-jvm-1
mini-jvm-1

0x03 簡單代碼實現

INTERPRETER-DEMO

寄存器

// 核心循環
  public void run() {

    List<Inst> insts = genInsts();

    int size = insts.size();
    int pc = 0;

    while (pc < size) {
      Inst inst = insts.get(pc);
      inst.execute();
      pc++;
    }
  }

// Add 指令實現
class AddInst implements Inst {
  public final Integer targetAddress;
  public final Integer[] sourceAddresses;

  AddInst(Integer targetAddress, Integer... sourceAddresses) {
    this.targetAddress = targetAddress;
    this.sourceAddresses = sourceAddresses;
  }

  @Override
  public void execute() {
    int sum = 0;
    for (Integer sourceAddress : sourceAddresses) {
      sum += RegisterDemo.REGISTER.get(sourceAddress);
    }
    RegisterDemo.REGISTER.put(targetAddress, sum);
  }
}
複製代碼

代碼地址: 寄存器 DEMO

// 核心循環
  public void run() {

    List<Inst> insts = genInsts();

    int size = insts.size();
    int pc = 0;

    while (pc < size) {
      Inst inst = insts.get(pc);
      inst.execute();
      pc++;
    }
  }

// Add 指令實現
class AddInst implements Inst {

  @Override
  public void execute() {
    Integer v2 = StackDemo.STACK.pop();
    Integer v1 = StackDemo.STACK.pop();
    StackDemo.STACK.push(v1 + v2);
  }
}
複製代碼

地址: 棧 DEMO

混合

// 核心循環
  public void run() {

    List<Inst> insts = genInsts();

    int size = insts.size();
    int pc = 0;

    while (pc < size) {
      Inst inst = insts.get(pc);
      inst.execute();
      pc++;
    }
  }

// Add 指令實現
class AddInst implements Inst {

  @Override
  public void execute() {
    Integer v2 = HybridDemo.STACK.pop();
    Integer v1 = HybridDemo.STACK.pop();
    HybridDemo.STACK.push(v1 + v2);
  }
}
複製代碼

地址: 混合 DEMO

關於上方代碼

針對具體場景實現, 恰好能用. 三個方案, 每一個方案均不超過 100 行代碼. 回上篇問題, 實現一個簡單的解釋器, 10 分鐘夠不夠?
天然是夠的, 有興趣不妨試着寫一下.

0x04 小結

本文討論瞭解釋器實現的三種方案, 並就簡單的案例分別實現了相應的解釋器.
mini-jvm 即是從這簡單的核心中慢慢擴展而來.
因爲 JVM 選擇的是混合方案, 後續的重點就只會在混合方案上了.

!!! 解釋器的核心實現尤其重要, 若是此文並無並無讓讀者理解, 必定是文章的問題, 歡迎提出你的問題, 已迭代此文.

0x05 預告

  1. 當前的解釋器跟 javac 編譯以後的 class 一毛錢的關係都沒有, 該有點關係了.

0x06 FAQ

  1. 爲何先從解釋器提及, 按照慣例, 不該該先說說 classfile 解析嗎?Classfile 解析並沒有任何難度, 體力活, 解析完的數據是爲解釋器服務的. 本質上是個靜態數據提供者. 非核心邏輯. 故而解釋器在前.
相關文章
相關標籤/搜索