凡是过往 皆为序章
之前两篇算是开端, 对解释器有个基本印象, 但是如何与 Java 世界关联起来, 似乎又有些模糊, 此篇正式进入 Java 世界.
按照惯例, 自然是要写个 HelloWorld , 对于构建一个简单的 JVM 来说, HelloWorld 会是个样子呢?
HelloWorld.java
public class HelloWorld { public static void main(String[] args) { int val = 1; System.out.println(val); } } 复制代码
案例如上图, 在控制台输出数字 1 .
[[ 为什么输出 1, 按照惯例不是应该输出 Hello World ? => 涉及到字符串的话, 程序就会复杂许多, 精简起见, 输出 1 已然足够 ]]
若是写 JVM , 那指令自然指的是 字节码指令, 自然是从 class 文件中解析而来.
如何生成 class 文件? 针对上面的案例, 可使用 javac 编译得到.
针对案例.
javac HelloWorld.java 复制代码
当前目录会生成 HelloWorld.class 文件.
class 文件本质上是一个更为紧凑的源码, 以便于机器解析.
如何查看 class 文件内容, 可以使用 JDK 自带工具 javap, javap 有很多选项, 暂时只关注 -c (对代码进行反汇编).
javap -c HelloWorld 复制代码
输出如下, 行号是额外添加的.
1 Compiled from "HelloWorld.java" 2 public class HelloWorld { 3 public HelloWorld(); 4 Code: 5 0: aload_0 6 1: invokespecial #1 // Method java/lang/Object."<init>":()V 7 4: return 8 9 public static void main(java.lang.String[]); 10 Code: 11 0: iconst_1 12 1: istore_1 13 2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 14 5: iload_1 15 6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 16 9: return 17 } 复制代码
连蒙带猜, 想必也能想到上方 11-16 行对应着源代码的 main 方法体内容. 其他内容可暂时忽略.
具体指令可参阅 官方说明
格式说明, [指令位置]: [指令] [指令参数]
e.g
0: iconst_1 , 指令位置 0, 指令为 iconst_1 , 无参数.
案例涉及到的指令说明
本质上是提供一个方法, 参数为 class 文件名, 结果为 解析后的指令集合.
public static List<Inst> parseInst(String classfilePath) { // 实现 } 复制代码
class 文件格式
官方 The ClassFile Structure 就案例而言, 了解即可.
上代码
List<Inst> insts = new ArrayList<>(); Inst inst = null; while (len > 0) { int code = is.readUnsignedByte(); switch (code) { case 0x03: inst = new IConst0(); break; case 0x04: inst = new IConst1(); break; case 0x3c: inst = new IStore1(); break; case 0x3d: inst = new IStore2(); break; case 0x10: inst = new Bipush(is.readByte()); break; case 0xa3: inst = new IfIcmpGt(is.readShort()); break; case 0x60: inst = new Iadd(); break; case 0x84: inst = new Iinc(is.readUnsignedByte(), is.readByte()); break; case 0xa7: inst = new Goto(is.readShort()); break; case 0x1b: inst = new ILoad1(); break; case 0x1c: inst = new ILoad2(); break; case 0xb1: inst = new Return(); break; case 0xb2: is.readUnsignedShort(); inst = new Getstatic(); break; case 0xb6: is.readUnsignedShort(); inst = new Invokevirtual(); break; default: throw new UnsupportedOperationException(); } len -= inst.offset(); insts.add(inst); } 复制代码
核心代码如上, 主要是根据不同的情况解析出不同的指令. 并不复杂, 体力活. 对照着官方文档解析即可得到.
与解释器联动起来, 解释上一步解析得到的指令, 由于解释器上篇已实现, 此处就不过多解释, 核心代码如下.
List<Inst> insts = parseInst(path + ".class"); // 由于 jvm 指令有步长的概念, 此处需要转为map. Map<Integer, Inst> instructions = genInstructions(insts); // 10 是临时写死, 实际应从 class 文件中解析得到. Frame frame = new Frame(10, 10); while (true) { int pc = frame.pc; Inst inst = instructions.get(pc); if (inst == null) { break; } inst.execute(frame); if (pc == frame.pc) { frame.pc += inst.offset(); } } 复制代码
与上篇提到的三个解释器大体相仿.
简单来讲, 就是个交换的问题, 用入参(即当前操作数栈的对象), 换一个返回值(放到当前栈顶)或者副作用(比如输入信息).
就案例来讲, 就是消耗掉栈的两个对象, 产生副作用(输出到控制台).
更复杂方法调用, 核心依然是上方的交换, 暂不讨论.
public void execute(Frame frame) { Object val = frame.operandStack.pop(); // 操作数栈顶, 即为要输出的值 Object thisObj = frame.operandStack.pop(); // 其次是 System.out 这个静态变量, 暂时忽略实现. System.out.println(val); // 利用宿主 JVM 输出. } 复制代码
$ java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo HelloWorld => 1 复制代码
求 1,2,3..100 的和, 并输出.
Sum100.java
public class Sum100 { public static void main(String[] args) { int sum = 0; for (int i = 1; i <= 100; i++) { sum += i; } System.out.println(sum); } } 复制代码
编译并解释
$ javac Sum100.java $ java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo Sum100 => 5050 复制代码
源码托管于 github, commit 传送门
git clone https://github.com/guxingke/demo.git cd demo/interpreter-demo mvn package # HelloWorld java HelloWorld.java java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo Sum100 javac Sum100.java java -cp target/interpreter-demo.jar com.gxk.demo.jvm.JvmDemo Sum100 复制代码
承接上篇, 使用单文件(300行代码)实现了一个简单的 JVM, 把 Java 世界 class 文件内的字节码指令解析出来, 并解释. 对于有兴趣入坑的同学来讲, 应该是个不错的案例.
如果想了解更多, 可以关注 mini-jvm 项目, 以上文提到的解释器为核心, Java 的一些语言特性基本实现.
系列还会有下一篇? 暂时不会有了, 解释器的核心已经就位, 一些语言特性就是逐步迭代了.
系列告一段落, 暂时不会更新了,个人杂记,难免会有疏漏错误, 如有兴趣, 有问题, 请反馈于我(评论,issue,邮件均欢迎). 再次感谢阅读.