我用 Rust 寫了一個 JVM
最近,我花了相當多的時間來學(xué)習(xí) Rust,就像任何有理智的人都會做的那樣,在編寫了幾個 100 行程序之后,我決定做一些更加雄心勃勃的事情——我用Rust寫了一個 Java 虛擬機。??
我在其中實現(xiàn)了很多獨創(chuàng)特性,我把它稱為『rjvm』。目前代碼已經(jīng)開源,各位可以在 GitHub 上獲取。
https://github.com/andreabergia/rjvm
我想強調(diào)的是,這是一個玩具型 JVM,是為了學(xué)習(xí)目的而構(gòu)建的,而不是一個嚴肅的實現(xiàn)。特別是,它不支持:
泛型
線程
反射
注釋
輸入/輸出
及時編譯器
字符串處理
實際上,我已經(jīng)實現(xiàn)了大多數(shù)重要的事情。比如:
控制流語句 ( if, for, ...)
原始和對象創(chuàng)建
虛擬方法和靜態(tài)方法調(diào)用
例外處理
垃圾收集
jar文件中的類解析
例如,以下是測試套件的一部分:
class StackTracePrinting {public static void main(String[] args) {Throwable ex = new Exception();StackTraceElement[] stackTrace = ex.getStackTrace();for (StackTraceElement element : stackTrace) {tempPrint(element.getClassName() + "::" + element.getMethodName() + " - " +element.getFileName() + ":" + element.getLineNumber());}}// We use this in place of System.out.println because we don't have real I/Oprivate static native void tempPrint(String value);}
我使用真實的包,它來自O(shè)penJDK 7 的rt.jar類。所以在上面的示例中,該類來自真實的 JDK!java.lang.StackTraceElement。
我對自己學(xué)到的關(guān)于 Rust 知識,以及如何實現(xiàn)虛擬機的知識感到非常滿意。
特別是,我非常興奮能夠?qū)崿F(xiàn)一個真正的、有效的垃圾收集器。它很普通,但它是我親自寫出來的,我很喜歡它。??
鑒于我已經(jīng)實現(xiàn)了最初的目標,我決定停止這個項目。我知道存在一些錯誤,但我并不打算修復(fù)它們。
概述
在這篇文章中,我將向您概述 JVM 的工作原理。
代碼組織
該代碼是一個標準的 Rust 項目。我把它分成了三個代碼空間(即包):
reader,它能夠讀取.class文件,并包含對其內(nèi)容進行建模以及各種數(shù)據(jù)類型;
vm,其中包含可以將代碼作為庫執(zhí)行的虛擬機;
vm_cli,其中包含一個非常簡單的命令行啟動器來運行虛擬機,可執(zhí)行java文件。
我正在考慮將reader提取到單獨的存儲庫中并將其發(fā)布到crates.io上,因為它實際上對其他的開發(fā)者可能有用。
解析.class文件
如大家所知曉的,Java 是一種半編譯語言 -javac編譯器獲取.java源文件并生成各種.class文件,通常壓縮在一個.jar文件中 - 這是一個zip文件. 因此,執(zhí)行一些 Java 代碼要做的第一件事就是加載一個.class文件,其中包含編譯器生成的字節(jié)碼。
其中,類文件包含如下內(nèi)容:
有關(guān)類的元數(shù)據(jù),例如其名稱或源文件名
超類名稱
實現(xiàn)的接口
字段及其類型與注釋
接下來是方法:
它們的描述符,它是一個字符串,表示每個參數(shù)的類型和方法的返回類型
元數(shù)據(jù),例如throws子句、注釋、泛型信息
字節(jié)碼以及一些額外的元數(shù)據(jù),例如異常處理程序表與行號表。
就像前面所描述的,我將rjvm創(chuàng)建了一個單獨的盒子,名為reader,它可以解析類文件,并返回一個對類及其所有內(nèi)容進行建模的Rust 結(jié)構(gòu)。
https://github.com/andreabergia/rjvm/blob/main/reader/src/class_file.rs
執(zhí)行方法
vm包的主要 API是Vm::invoke,用于執(zhí)行方法。
它需要一個CallStack,其中包含各種CallFrame, 一個用于正在執(zhí)行的每個方法。對于執(zhí)行main,調(diào)用堆棧最初將為空,并且將創(chuàng)建一個新的棧幀來運行它。接下來,每次函數(shù)調(diào)用都會向調(diào)用堆棧添加一個新幀。當方法執(zhí)行完成時,其相應(yīng)的幀將被丟棄,并從調(diào)用堆棧中刪除。
大多數(shù)方法將用 Java 實現(xiàn),因此它們的字節(jié)碼將被執(zhí)行。但是,rjvm也支持本機方法,即直接由 JVM 實現(xiàn)而不是在 Java 字節(jié)碼中實現(xiàn)的方法。其中相當多的部分位于 Java API 的“較低部分”,需要與操作系統(tǒng)進行交互(例如執(zhí)行 I/O)或支持運行時。
你可能見過的后者的一些示例包括System::currentTimeMillis、System::arraycopy或Throwable::fillInStackTrace。在 中rjvm,這些是由Rust 函數(shù)實現(xiàn)的。
JVM是基于堆棧的虛擬機,即字節(jié)碼指令主要在堆棧上操作。還有一組由索引標識的局部變量,可用于存儲值并將參數(shù)傳遞給方法。這些與 中的每個調(diào)用幀相關(guān)聯(lián)rjvm。
值與對象建模
類型Value對局部變量、堆棧元素或?qū)ο笞侄蔚目赡苤颠M行建模,并按如下方式實現(xiàn):
/// Models a generic value that can be stored in a local variable or on the stack.#[derive(Debug, Default, Clone, PartialEq)]pub enum Value<'a> {/// An unitialized element. Should never be on the stack,/// but it is the default state for local variables.#[default]Uninitialized,/// Models all the 32-or-lower-bits types in the jvm: `boolean`,/// `byte`, `char`, `short`, and `int`.Int(i32),/// Models a `long` value.Long(i64),/// Models a `float` value.Float(f32),/// Models a `double` value.Double(f64),/// Models an object valueObject(AbstractObject<'a>),/// Models a null objectNull,}
順便說一句,這里的 sum 類型(如 Rust 的enum)是一種美妙的抽象——它非常適合表達一個值可能具有多種不同類型的情況。
為了存儲對象和它的值,我最初實現(xiàn)了一個名為Object 的簡單結(jié)構(gòu),Object其中包含對類的引用(使用對象類型進行建模)和Vec<Value>用來存儲字段值 。
在我實現(xiàn)垃圾收集器時,我修改了它并以使用較低級別的實現(xiàn),里帶有大量的指針和強制轉(zhuǎn)換 - 相當 C 語言風(fēng)格!
在當前的實現(xiàn)中,一個 AbstractObject(模擬“真實”對象或數(shù)組)是指向字節(jié)數(shù)組的指針,其中包含幾個標頭字,然后才是字段值。
執(zhí)行指令
執(zhí)行一個方法意味著一次執(zhí)行一個字節(jié)碼指令。
JVM 有著大量的指令(超過 200 條!),由字節(jié)碼中的一個字節(jié)進行編碼。許多指令后面都有參數(shù),有些指令的長度是可變的。
這是在代碼中通過Instruction類型建模:
/// Represents a Java bytecode instruction.pub enum Instruction {Aaload,Aastore,Aconst_null,Aload(u8),// ...
如上所述,方法的執(zhí)行將保留一個堆棧和一個局部變量數(shù)組,指令通過其索引引用它們。此外,它還會將程序計數(shù)器初始化為零,即下一條要執(zhí)行的指令地址。該指令將被處理并更新程序計數(shù)器 ,通常情況是加 1,但各種跳轉(zhuǎn)指令可以將其移動到不同的位置。
這些用于實現(xiàn)所有流控制語句,例如if、for或while語言。
一個特殊的指令系列由那些可以調(diào)用另一種方法的指令組成。
有多個方法可以解決如應(yīng)該調(diào)用哪個方法的方案。其中虛擬或靜態(tài)查找是主要方法,但還有其它方法。
當解析完正確的指令后,rjvm將向調(diào)用堆棧添加一個新幀,并立即開始該方法的執(zhí)行。特殊的情況,如果該方法的返回值是void,它將被推送到堆棧,并且將恢復(fù)執(zhí)行。
Java 字節(jié)碼格式相當有趣,我后面有計劃專門寫一篇文章向大家來介紹各種指令。
例外與異常處理
異常的實現(xiàn)是相當復(fù)雜的,因為它們破壞了正常的控制流,并且可能從方法中提前返回(并在調(diào)用堆棧上傳播)。
不過,我對實現(xiàn)它們的方式非常滿意,這里向各位展示一些相關(guān)代碼。
你需要知道的第一件事是,任何catch塊都對應(yīng)于方法異常表的一個條目,每個條目包含程序計數(shù)器范圍、catch 塊中第一條指令的地址以及該塊所處理的異常的類名稱捕獲。
接下來,CallFrame::execute_instruction的簽名如下:
fn execute_instruction(&mut self,vm: &mut Vm<'a>,call_stack: &mut CallStack<'a>,instruction: Instruction,) -> Result<InstructionCompleted<'a>, MethodCallFailed<'a
其中類型為:
/// Possible execution result of an instructionenum InstructionCompleted<'a> {/// Indicates that the instruction executed was one of the return family. The caller/// should stop the method execution and return the value.ReturnFromMethod(Option<Value<'a>>),/// Indicates that the instruction was not a return, and thus the execution should/// resume from the instruction at the program counter.ContinueMethodExecution,}/// Models the fact that a method execution has failedpub enum MethodCallFailed<'a> {InternalError(VmError),ExceptionThrown(JavaException<'a>),}
標準 Rust的Result類型是:
enum Result<T, E> {Ok(T),Err(E),}
因此,執(zhí)行一條指令會導(dǎo)致四種可能的狀態(tài):
指令執(zhí)行成功,當前方法可以繼續(xù)執(zhí)行(標準情況);
該指令執(zhí)行成功,并且它是一個返回指令,因此當前方法應(yīng)該返回(可選)一個返回值;
該指令無法執(zhí)行,可能發(fā)生了一些內(nèi)部VM錯誤;
或者指令無法執(zhí)行,因為拋出了標準 Java 異常。
執(zhí)行方法的代碼如下:
/// Executes the whole methodimpl<'a> CallFrame<'a> {pub fn execute(&mut self,vm: &mut Vm<'a>,call_stack: &mut CallStack<'a>,) -> MethodCallResult<'a> {self.debug_start_execution();loop {let executed_instruction_pc = self.pc;let (instruction, new_address) =Instruction::parse(self.code,executed_instruction_pc.0.into_usize_safe()).map_err(|_| MethodCallFailed::InternalError(VmError::ValidationException))?;self.debug_print_status(&instruction);// Move pc to the next instruction, _before_ executing it,// since we want a "goto" to override thisself.pc = ProgramCounter(new_address as u16);let instruction_result =self.execute_instruction(vm, call_stack, instruction);match instruction_result {Ok(ReturnFromMethod(return_value)) => return Ok(return_value),Ok(ContinueMethodExecution) => { /* continue the loop */ }Err(MethodCallFailed::InternalError(err)) => {return Err(MethodCallFailed::InternalError(err))}Err(MethodCallFailed::ExceptionThrown(exception)) => {let exception_handler = self.find_exception_handler(vm,call_stack,executed_instruction_pc,&exception,);match exception_handler {Err(err) => return Err(err),Ok(None) => {// Bubble exception up to the callerreturn Err(MethodCallFailed::ExceptionThrown(exception));}Ok(Some(catch_handler_pc)) => {// Re-push exception on the stack and continue// execution of this method from the catch handlerself.stack.push(Value::Object(exception.0))?;self.pc = catch_handler_pc;}}}}}}}
我知道這段代碼中有相當多的實現(xiàn)細節(jié),但我希望它能讓大有了解如何使用 Rust的Result和模式匹配很奇妙地映射到上述行為。
不得不說我對自己寫的這段代碼感到由衷地自豪。??
垃圾收集
rjvm最后的里程碑是垃圾收集器的實現(xiàn)。
我選擇的算法是一個停止世界。原因很簡單,因為沒有線程!先實現(xiàn)半空間復(fù)制收集器。
我已實現(xiàn)了切尼算法(https://en.wikipedia.org/wiki/Cheney%27s_algorithm)的一個(較差的)變體,但我真的應(yīng)該去實現(xiàn)真正的東西......??
這個算法是將可用內(nèi)存分成兩部分,稱為半空間:一部分將處于活動狀態(tài)并用于分配對象,另一部分將不再使用。
當空間滿了的時候,將觸發(fā)垃圾收集,所有活動對象將被復(fù)制到另一個半空間。然后,所有對象的引用都將被更新,以便它們被指向新的副本。最后,兩者的角色將互換——類似于藍綠(https://www.redhat.com/en/topics/devops/what-is-blue-green-deployment)部署的工作原理。




該算法具有以下特點:
很顯然,它浪費了大量內(nèi)存(就是最大內(nèi)存的一半?。?;
分配速度非??欤ㄅ鲎仓羔槪?/span>
復(fù)制和壓縮對象,意味著它不必處理內(nèi)存碎片;
由于更好的緩存行利用率,壓縮對象可以提高性能。
當然,真正的 Java VM 使用更復(fù)雜的算法,通常是分代垃圾收集器,例如 G1 或并行 GC,它們使用復(fù)制策略的演變版本。
結(jié)論
在寫rjvm的過程中,我學(xué)到了很多,得到了甚多樂趣。當然,不能要求從副業(yè)項目中得到更多......也許下次我會選擇一些不那么雄心勃勃的東西,來學(xué)習(xí)另一門新的編程語言!??
順便再說一句,我想說從 Rust 中獲得了非常多的樂趣。我認為它是一種很棒的語言,正如我之前寫的那樣,我很喜歡使用它來實現(xiàn)自己的JVM!
編譯:洛逸
作者:安德里亞
https://andreabergia.com/blog/2023/07/i-have-written-a-jvm-in-rust/
推薦閱讀:
不是你需要中臺,而是一名合格的架構(gòu)師(附各大廠中臺建設(shè)PPT)
企業(yè)IT技術(shù)架構(gòu)規(guī)劃方案
論數(shù)字化轉(zhuǎn)型——轉(zhuǎn)什么,如何轉(zhuǎn)?
