用Java写一个能跑真实字节码的虚拟机,核心不是性能,而是把class文件解析、运行时数据区和指令执行三件事串起来。
Java写虚拟机难吗?难点不在Java,在规范细节
很多人一听“用Java写虚拟机”,第一反应是这东西肯定特别难,甚至觉得要碰到底层C语言才行,如果你只是想写一个教学级、能执行简单字节码的虚拟机,Java反而是最合适的语言之一,真正让人头疼的不是语言本身,而是JVM规范的抽象程度和细节密度。
一个典型的Java开发者天天和JVM打交道,但对class文件内部长什么样、字节码指令怎么压栈、方法调用时帧怎么切换,多数情况下并没有直观认识,用Java写虚拟机,相当于把你每天用的那个黑盒子拆开,看看里面到底怎么转,这个过程会倒逼你去读规范、翻字节码、盯着操作数栈的变化调试。
难度可以分层:如果只实现int类型的加减乘除、局部变量读写、静态方法调用和返回,难度是可控的,周末加几个晚上就能跑通,如果你想完整实现全部200多条指令、异常表、同步、invokedynamic、垃圾回收,那工作量和复杂度会呈指数级上升,所以关键不是问“java写虚拟机难吗”,而是先明确你打算写到哪个边界。
用Java实现虚拟机教程:先读懂class文件再动手
据Oracle发布的《Java虚拟机规范》,class文件是一组严格的二进制结构,很多人一上来就写执行引擎,结果卡在常量池解析上,正确的顺序应该是:先读class,再建数据区,最后执行指令。
第一步:把class文件当成二进制结构读
class文件开头是魔数CAFEBABE,后面跟着版本号、常量池计数、常量池项、访问标志、字段、方法、属性,你可以用DataInputStream按顺序读取u1、u2、u4,每读一个字节就记录下来。
建议先用现成工具建立直观感受:
javac Hello.java编译出class文件javap -c Hello查看字节码指令javap -v Hello查看常量池完整信息
写一个ClassFileParser
类,把常量池中的CONSTANT_Utf8、CONSTANT_Class、CONSTANT_Methodref、CONSTANT_NameAndType逐项解析,存到List或Map里,这一步不涉及任何高深算法,就是一个细心的二进制拆包过程。
第二步:搭出运行时数据区
JVM运行时有堆、虚拟机栈、方法区、程序计数器等区域,用Java写虚拟机时,不必死磕内存模型,可以用简单结构模拟:
- 方法区用
Map<String, ClassFile>存放加载后的类 - 虚拟机栈用
Deque<StackFrame>模拟方法调用链 - 每个
StackFrame内部维护一个局部变量表和一个操作数栈
操作数栈可以用ArrayDeque<Integer>或int[]加栈顶指针,局部变量表用int[],因为Java的局部变量在字节码层面有确定索引,这个设计思路和真实JVM一致,只是你用Java容器代替了裸内存。
第三步:用最少指令跑通一个方法
JVM指令集有200多条,但教学级只需要实现20到30条常用指令就能跑通简单程序,优先实现这些:
- 常量入栈:
iconst_0到iconst_5、bipush、sipush - 局部变量读写:
iload、istore - 算术运算:
iadd、isub、imul、idiv、irem - 方法返回:
ireturn、return - 方法调用:
invokestatic
指令分派可以用switch-case,把字节码操作码对应到具体逻辑,例如iadd就是弹出两个操作数,相加后压回,每实现一条指令,立刻用javap生成的字节码对照验证,比闷头写要快得多。
Java写虚拟机需要什么基础?三块知识要提前补
Java二进制IO与集合
读取class文件离不开DataInputStream、ByteBuffer,解析常量池离不开List、Map,这些是Java日常开发的基本功,但很多人对二进制读取并不熟练,建议先写一个小工具,把class文件的前20个字节按十六进制打印出来,找找感觉。
JVM规范里的常量池必须吃透
常量池是class文件里最复杂的部分之一,方法调用、字段引用、字符串字面量都靠它描述,你不能只记结论,要能手动解析一个CONSTANT_Methodref,知道它引用哪个CONSTANT_Class和哪个CONSTANT_NameAndType,常量池解析错了,后面方法调用一定跑不通。
操作数栈和局部变量表的切换逻辑
栈帧是虚拟机执行的基本单位,每次进入方法,创建一个新栈帧;方法返回时,栈帧销毁,返回值压到上一个栈帧的操作数栈里,局部变量表索引从0开始,long和double占两个槽位,这些规则不复杂,但要在写代码前用纸笔推演几次,否则调试时会很痛苦。
自己用Java写一个简单虚拟机:最小可运行路径
如果你不想一上来就啃完整JVM规范,可以先写一个最小可运行版本,目标是执行类似下面的Java代码:
public class Calc {
public static int add(int a, int b) {
return a + b;
}
}
功能边界先收紧
第一版只支持:
int类型- 静态方法
- 无异常处理
- 20条左右整型指令
这样范围小、反馈快,一个周末就能看到虚拟机输出正确结果,不要一上来就做对象、继承、多态,那会让调试成本翻倍。
实操命令验证
写完后用以下流程验证:
- 编译
Calc.java得到Calc.class - 你的虚拟机读取class文件,执行
add(3, 5) - 输出结果应该是
8 - 和
java Calc的行为做对比
如果能稳定输出结果,说明class解析、常量池、栈帧、指令执行四个环节已经打通。
方法调用是最容易翻车的环节
invokestatic执行时要做四件事:找到目标方法的字节码、创建新栈帧、把实参从当前操作数栈复制到新栈帧的局部变量表、切换当前栈帧,方法返回时,如果返回值是int,还要把返回值压回调用者的操作数栈,任何一步顺序搞错,结果就会出错,调试时可以在每次压栈、弹栈、切换帧时打印栈深度和局部变量表内容。
用Java写一个JVM要多久?时间规划参考
教学级虚拟机的时间投入主要取决于你的目标边界,行业共识认为,单人完整实现商用级JVM难度极大,但做一个能跑简单程序的教学版是可行的。
| 功能范围 | 指令数量 | 预计时间 | 适合人群 |
|---|---|---|---|
| 只能执行int算术和静态方法 | 20-30条 | 周末两三天 | 想快速建立概念 |
| 支持对象、字段、实例方法 | 60-80条 | 三到四周 | 想深入理解面向对象底层 |
| 支持异常、数组、字符串拼接 | 120-150条 | 两到三个月 | 想写一个更像JVM的东西 |
第一周重点读class文件和常量池,第二周搭运行时数据区,第三周实现指令分派,第四周打通方法调用,不要每天空想,保持每天有可运行的最小版本,比一次写一大堆再调试要高效得多。
Q&A:java写虚拟机常见疑问
Java写虚拟机用什么资料入门比较靠谱?
最靠谱的资料是Oracle官方发布的《The Java Virtual Machine Specification》,免费可读,配合javap -v命令反编译真实class文件,把规范和字节码对照着看,不要只看博客,官方规范对常量池标签、指令操作码描述得最准确。
Java写虚拟机需要先学编译原理吗?
不需要深入编译原理,JVM字节码是编译后的产物,你只需要理解栈式指令集和符号引用,但如果后续想写一个把Java源码编译成字节码的编译器,那才需要系统学习编译原理。
用Java写虚拟机对日常工作有什么用?
最直接的好处是排查线上问题会更加从容,遇到ClassNotFoundException、NoSuchMethodError、StackOverflowError时,你能从class加载、常量池、栈帧的角度理解真正原因,另一个好处是阅读字节码的能力会提升,做性能优化或字节码增强时不会一头雾水,把虚拟机的每一步都亲手执行一遍,你对Java平台的理解会从“会用API”切换到“看得懂底层”。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/661815.html





