如何用 Java 寫一個 Java 虛擬機器器

2023-07-28 21:00:31

github 專案連結

https://github.com/FranzHaidnor/haidnorJVM

haidnorJVM

使用 Java17 編寫的 Java 虛擬機器器

意義

  1. 紙上得來終覺淺,絕知此事要躬行。只學習 JVM 機制和理論,很多時候任然覺得缺乏那種大徹大悟之感
  2. 使用簡單的方式實現 JVM,用於學習理解 JVM 執行原理

主要技術選型

實現功能

  • 實現了 99% 的 JVM 位元組碼指令。參照 JVM 位元組碼規範實現 The Java Virtual Machine Instruction Set
  • 支援算數運運算元 (+,-,*,^,%,++,--)
  • 支援關係運算子 (==,!=,>,<,>=,<=)
  • 支援位運運算元 (&,|,^,~,<<,>>,>>>)
  • 支援賦值運運算元 (=,+=,-=,*=,%=,<<=,>>=,&=,^=,|=)
  • 支援 instanceof 運運算元
  • 支援迴圈結構程式碼 (while,do...while,for,foreach)
  • 支援條件結構程式碼 (if,if...else,if...else if)
  • 支出建立自定義類
  • 支援建立物件、存取物件
  • 支援抽象類
  • 支援多型繼承、介面
  • 支援存取靜態方法
  • 支援存取物件方法
  • 支援 JDK 中自帶的 Java 類
  • 支援反射
  • 支援異常
  • 列舉 (開發中...)
  • switch 語法 (開發中...)
  • lambda 表示式 (開發中...)

侷限性

  • 不支援多執行緒
  • 不支援多維陣列
  • 暫無雙親委派機制實現
  • 無垃圾收集器實現。垃圾回收依靠宿主 JVM

快速體驗

你需要準備什麼

  1. 整合式開發環境 (IDE)。你可以選擇包括 IntelliJ IDEA、Visual Studio Code 或 Eclipse 等等
  2. JDK 17。並設定 JAVA_HOME
  3. JDK 8。haidnorJVM 的主要目標是執行 Java8 本版的位元組碼檔案。(但 haidnorJVM 沒有強制要求位元組碼檔案是 Java8 版本)
  4. Maven

設定 haidnorJVM

設定紀錄檔輸出級別

resources\simplelogger.properties 檔案中修改紀錄檔輸出級別,一般使用 debuginfo

  • 設定 info 級別將不會看到任何 haidnorJVM 內部執行資訊
  • 設定 debug 級別下執行將會非常友好的輸出 JVM 正在執行的棧資訊
public class Demo5 {

    public static void main(String[] args) {
        String str = method1("hello world");
        method1(str);
    }

    public static String method1(String s) {
        return method2(s);
    }

    public static String method2(String s) {
        return method3(s);
    }

    public static String method3(String s) {
        System.out.println(s);
        return "你好 世界";
    }
    
}

每一個 結構圖形,都表示一個 JVM 執行緒棧中的棧幀

設定 rt.jar 路徑

修改 haidnorJVM.properties 檔案中的內容。設定 rt.jar 的絕對路徑,例如rt.jar=D:/Program Files/Java/jdk1.8.0_361/jre/lib/rt.jar

執行單元測試用例

在 IDE 中開啟專案中 test 目錄下的 haidnor.jvm.test.TestJVM.java 檔案。 這是 haidnorJVM 的主要測試類, 裡面的測試方法可以解析載入執行 .class 位元組碼檔案。

public class TestJVM {
   /**
    *  haidnorJVM 會載入 HelloWorld.java 在 target 目錄下的編譯後的位元組碼檔案,然後執行其中的 `main(String[] args)` 方法。
    *  你可以使用打斷點的方式看到 haidnorJVM 是如何解釋執行 Java 位元組碼的。
    *  值得注意的是,這種方式編譯執行的位元組碼檔案是基於 java17 版本的。
    */
   @Test
   public void test() {
      runMainClass(HelloWorld.class);
   }
}

執行 .class 檔案

  1. 使用 maven 命令將 haidnorJVM 編譯打包,得到 haidnorJVM.jar 檔案
  2. 編寫一個簡單的程式,例如以下程式碼
public class HelloWorld {
   public static void main(String[] args) {
     System.out.println("HelloWorld");
   }
}
  1. 編譯程式碼,得到 HelloWorld.class 檔案 (推薦使用 JDK8 進行編譯)
  2. 使用 haidnorJVM 執行程式。執行命令 java -jar haidnorJVM.jar -class R:\HelloWorld.class。注意! 需要 class 檔案的絕對路徑

執行 .jar 檔案

  1. 使用 maven 命令將 haidnorJVM 編譯打包,得到 haidnorJVM.jar 檔案
  2. 編寫一個 java 專案編譯打包成 .jar 檔案,例如 demo.jar。要求 .jar 檔案中的 META-INF/MANIFEST.MF 檔案內有 Main-Class 屬性 (含有 public static void main(String[] args) 方法的主類資訊)
  3. 使用 haidnorJVM 執行程式。執行命令 java -jar haidnorJVM.jar -class R:\demo.jar。注意! 需要 jar 檔案的絕對路徑

存在的問題

由於 haidnorJVM 目前執行 JDK 自帶的類是使用反射解決的,因此 haidnorJVM 使用 JDK17 執行部分 JDK 自帶的類時會存在一些問題,例如執行以下程式碼將會丟擲異常

public class Demo {
    public static void main(String[] args) {
        List<Integer> list = List.of(1, 2, 3, 4, 5);
        list.add(6);
    }
}
java.lang.reflect.InaccessibleObjectException: Unable to make public boolean java.util.ImmutableCollections$AbstractImmutableCollection.add(java.lang.Object) accessible: module java.base does not "opens java.util" to unnamed module @18769467

	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
	at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)

它表示嘗試通過反射來存取一個方法或欄位,但該方法或欄位的可存取性限制導致無法存取。

這個限制通常是由於 Java 模組系統引起的。模組系統允許將程式碼劃分為獨立的模組,
並控制模組之間的存取許可權。以上異常的原因是 module java.base does not "opens java.util" to unnamed module,也就是說 java.base 模組沒有向未命名模組開放 java.util 包

解決方法:
啟動 haidnorJVM 時新增 JVM 引數 --add-opens java.base/java.util=ALL-UNNAMED 繞過存取性限制

聯絡作者


微訊號: haidnor