[Golang实现JVM第三篇] 解释器雏形

在上一篇中我们已经完成了class文件的解析工作,虽然没有解析所有的属性,但是已经足够支持一些基本的算法题Java代码编译生成的class文件了。有了这一步,日后如果遇到新的特性需要支持,只需缺哪补哪,补上对应属性的解析逻辑就可以了。下一步就是实现一个基本的执行引擎,即解释器,并且支持基本的栈操作相关的指令,比如iconst_x, istore_x, bipush等。

基于栈的指令集和基于寄存器的指令集

JVM字节码是一套基于栈的指令集,也就是说操作数栈是一切计算的基本容器,大部分指令都是围绕着操作数栈展开的。相对应的还有一种基于寄存器的指令集,这种指令集的特点是指令中就携带寄存器地址,对寄存器进行操作,优点指令短小精悍,执行效率高,缺点是会依赖于特定的硬件,可移植性差。栈指令集的优缺点则刚好相反,因为栈是一种抽象数据结构而不是具体的硬件设施,因此可移植性强,但是指令的数量往往比较臃肿,执行效率相对较低。

举个例子,计算1+1,用Javac编译后的对应字节码是这样的:

iconst_1
iconst_1
iadd
istore_0

其中前两条iconst_1表示将整数1压入操作数栈,这时候操作数栈就有两个1了。iadd指令则表示从操作数栈中连续出栈两次,将值相加,最后再把结果压入栈中。istore_0则表示将栈顶元素(计算结果2)出栈,存入本地变量表索引为0的槽位中。可以看到,明明是很简单的计算,却多出了很多压栈出栈操作。

如果是基于寄存器的指令集,那一般会长这样:

mov exa,1
add exa,1

即先将1存入exa寄存器,然后直接把寄存器中的值+1完成计算,指令数量少了很多。

不过并不是说JVM的字节码一定就会比寄存器指令慢,毕竟JVM 中有大量的优化,字节码可能会被省略、被乱序执行,或者直接被JIT编译成本地语言,也就是基于寄存器的指令了。当然,优化并不在我们实现JVM的目标范围内。

解释器实现思路

想实现JVM的朋友应该都对JVM的基本构成有了解了,什么方法区、堆区、方法栈等等。还是那句话,千万不要一上来就考虑这么复杂的因素,这样只会掉坑里爬不出来,正确的方法是先实现一个最简单的能跑的例子,然后再根据真实的JVM结构慢慢扩充。比如,对于字节码iconst_1来说,他的含义就是将整数1压如操作数栈,那么实现一个栈,遇到这条指令就压栈不就完事了吗?什么方法栈、堆区、类加载器、垃圾回收、线程调度,现阶段通通都不要考虑,随着字节码越来越复杂,这些总会有的。

一个解释器应该具备的最基本的要素,就两条,一是死循环,二是指向下一条指令的程序计数器(Program Counter, 简称PC),golang伪代码如下:

pc := 0
for {
   
   
  byteCode := code[pc] // 取出pc指向的指令
  execute(byteCode, &pc) // 执行指令,同时传入PC的指针,因为执行的过程可能需要修改pc的值
  if 结束? {
   
   
    break
  }
}

我们可以先定义一个结构体MiniJvm来表示一个JVM:

// VM定义
type MiniJvm struct {
   
   
	// 方法区
	MethodArea *MethodArea

	// MainClass全限定性名
	MainClass string

	// 执行引擎
	ExecutionEngine ExecutionEngine

	// 本地方法表
	NativeMethodTable *NativeMethodTable

	// 保存调用print的历史记录, 单元测试用
	DebugPrintHistory []interface{
   
   }
}

这里有很多现阶段用不到的字段,忽略即可,等解释到对应的字节码以后再回头加上;

然后我们定义出执行引擎接口,为啥用接口呢,因为现在是解释的,万一以后牛逼了想搞个编译的呢?

type ExecutionEngine interface {
   
   
	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值