从零编写一个JVM虚拟机,核心路径是:先实现类加载器与运行时数据区,再完成字节码解释执行引擎,最后分阶段补充垃圾回收和即时编译,整个周期大约需要3到6个月。
这条路并不轻松,但也不像想象中那么神秘,JVM本质上是一台基于栈的抽象计算机,它读取.class文件中平台无关的字节码,在自己的内存模型里完成对象创建、方法调用和指令执行,下面我会按照实际动手的顺序,拆解每个阶段的落地步骤。
编写jvm虚拟机前要掌握哪些核心知识?
在敲第一行代码之前,你需要先扫清几个前置障碍,行业共识认为,扎实的计算机基础比丰富的Java经验更重要。
硬件层:CPU如何执行指令
JVM模拟的正是CPU的工作方式:取指令、解码、执行、更新程序计数器,熟悉寄存器、内存地址、栈帧的概念,会让你在后面对JVM运行时数据区设计时更有手感,不必精通汇编,但至少要理解“栈”和“寄存器”这两种指令模型的差异JVM是典型的栈式指令集架构。
Java语言层:.class文件到底长什么样
用javap -c或javap -verbose反编译一个简单的类文件,逐字节对照《Java虚拟机规范》阅读魔数、版本号、常量池、方法表的结构,这里没有捷径,字节码格式是JVM实现者的第一道硬门槛。
操作数栈与局部变量表:理解JVM的执行模型
JVM每条指令都在操作数栈上压入或弹出数据,局部变量表则存放方法参数和内部变量,先手动模拟iadd、invokevirtual、return几条指令在栈上流转的过程,再动笔写代码,效率会提高一大截。
操作系统层:内存与线程的映射
你的虚拟机跑在操作系统进程里,Java线程最终会映射为原生线程,处理好栈溢出、内存分配失败这几个异常场景,是后续做GC前置条件,据可靠的公开技术资料,HotSpot虚拟机自身代码量超过百万行,而你不需要复制这个规模,只需要实现规范定义的最小可用子集。
从零写jvm:自研JVM的整体架构怎么设计?
我建议把项目切分成四个相对独立的模块,模块之间用清晰接口解耦,这样每一阶段都有可验证的成果,不会中途失去信心。
| 模块 | 核心职责 | 输出产物 |
|---|---|---|
| 类加载子系统 | 读取.class文件,解析字节码,完成链接 | Class对象 |
| 运行时数据区 | 管理方法区、堆、虚拟机栈、程序计数器 | 内存模型 |
| 执行引擎 | 逐条解释字节码指令,执行方法调用 | 程序运行结果 |
| 本地方法接口 | 对接操作系统能力(可选,后期实现) | native方法支持 |
第一步:搭建最小的类加载器
从读取文件路径开始,解析.class的二进制数据,定义ClassFile结构体,依次读入魔数0xCAFEBABE、次版本号、主版本号、常量池计数器和各个常量项,实现常量池解析后,你可以先不加载任何方法,只是打印出类名、字段名和方法签名,验证字节码读取是否正确。
第二步:设计运行时数据区
这一阶段的目标是让”Hello World”跑起来,你至少需要实现:
- 方法区:存储类元信息和常量池的运行时表示
- 堆:一个简单的对象数组或链表结构,广播分配指针
- 虚拟机栈:每次方法调用创建一个栈帧,包含局部变量表、操作数栈和帧数据
jvm虚拟机怎么实现字节码执行引擎?
架构搭好后,执行引擎是重头戏,所谓”写一个JVM”,在绝大部分工作中就是在写这个模块。
第三步:实现指令集的分发循环
主循环大概长这样:
while (pc 在合法范围内) {
opcode = 读取下一个字节
switch (opcode) {
case 0x00: nop; break;
case 0x1B: iload; break;
case 0x60: iadd; break;
// 约250个指令...
case 0xB6: invokevirtual; break;
}
pc += 当前指令长度;
}
这里最烧时间的是操作数栈的类型管理,比如iload从局部变量表取出一个int压栈,invokevirtual需要根据方法描述符计算参数个数并调整栈帧,静下心,按指令类别逐批实现:先做加载/存储指令,再做算术运算,接着是对象创建和字段访问,最后完善方法调用。
第四步:方法调用和栈帧切换最钻牛角尖的部分
Java方法调用在字节码层面由四条指令驱动:invokestatic、invokespecial、invokevirtual、invokeinterface,其中invokevirtual要考虑方法重写,在运行期根据对象的实际类型查找方法解析入口,我在实现这个过程时,建议将方法符号引用解析做成独立模块,而不是混在执行逻辑里,否则后续加上接口默认方法时会异常痛苦。
第五步:异常处理表(Exception Table)
JVM的异常处理不是简单的”try-catch”语法翻译,而是在方法属性中维护一张异常表,每个条目包含start_pc、end_pc、handler_pc和catch_type,执行引擎遇到athrow指令时,逆序遍历异常表寻找匹配的处理器,这一功能最好在基础指令跑通后立刻补上,因为后续测试框架要大量依赖异常来报告错误。
自己写一个jvm需要做垃圾回收吗?
这个问题很多初学者会纠结,我的答案是:第一版完全不需要,垃圾回收与执行引擎的耦合度极低,你可以先让对象堆只增不减,用简单标记清除处理内存耗尽的情况,但如果你想深入学习GC,以下两个版本的演进路径可供参考。
第六步:从标记-清除到复制收集器
在实现了完整的类加载和大部分字节码指令后,你可以为堆区加上二分复制GC:对象分配采用bump-pointer,GC时遍历对象图,存活对象复制到对半空间,这个算法实现最简单、无需移动大量内存,也是HotSpot新生代Serial收集器的原理原型,业内专家指出,实现GC的最大难度在于准确识别对象引用你必须维护栈帧中哪些槽位是引用类型,否则无法遍历对象图,建议在类文件的字段表里记录字段类型描述符,并在栈帧中额外存放局部变量列表的类型标记。
第七步:即时编译(JIT)先了解原理就好
解释执行能让你拥有一台可用的虚拟机,但性能远不及生产级JVM,即时编译在入门阶段不必实现,先理解”热点代码识别+编译为本地指令”的原理即可,自己动手阶段,用-Xint参数强制纯解释模式运行,与自带JVM做功能对比,比纠结JIT实现方式更有价值。
零基础写jvm虚拟机避坑指南:实操路径与推荐参考
语言选择与项目搭建
实现JVM的首选语言是Java,因为你可以直接复用JDK的java.lang.Object等基础类库,备选方案是C++或Rust,后者能让你对内存布局有更精确的控制,但在运行时数据区的实现上会花更多调试时间。
测试驱动的开发节奏
在每一个模块完成后,立刻用一个小型测试用例验证,我习惯把测试样例按指令类别分类维护,比如算术运算/ArithTest.java、方法调用/InvokeTest.java。特别注意使用-Xss参数调低栈容量,这样能在自己的虚拟机上快速暴露栈帧泄漏问题。
查阅规范的姿势
《Java虚拟机规范》(Java SE 8版或17版)是权威依据,遇到指令语义模糊时,查看HotSpot源码中
bytecodeInterpreter.cpp对应的处理逻辑,这里有一个较大的比例情况:社区答疑不如规范原文可靠,少看博客文章,多看规范文档。
零基础写jvm需要准备哪些工具?
- JDK自带工具:
javac编译测试源码,javap反查验格式 - 十六进制编辑器:必要时直接查看.class二进制内容
- 对比虚拟机:目标版本JDK自带的java命令,作为输出对照
- 调试器:趁手的IDE断点调试执行引擎
这些工具不需要额外安装,JDK和IDE就够用了。
常见问题
自己动手写JVM最少需要实现多少条指令?
Java语言规范要求编译器生成的字节码大约涉及200多条指令,但一个最小可用的JVM可以只实现约50条核心指令,覆盖变量加载存储、算术运算、对象创建、方法调用和返回,用这个子集已经可以运行简单的递归程序和控制流逻辑,后续不需要一次性完成全部指令,可以按需补充。
jvm虚拟机源码推荐研究哪个项目?
如果你是Java开发者,推荐研究JVM开源项目中的/src/main/java目录,学习顺序建议先看CLI入口,再看运行时数据区定义,最后才是执行引擎,对比自己的实现时,重点关注对方如何处理方法调用的栈帧切换,如果偏好代码量较小的参考项目,可以查看GitHub上star较高的mini-jvm类项目,它们通常只有几千行代码,清晰展示了主循环与操作数栈的结构。
写一个JVM对工作面试有帮助吗?
有参考价值,在面试Java后端岗位时,完整实现过JVM核心模块的经历能从两个维度证明你的能力:一是底层功底,说明你理解类加载机制、内存模型而非停留在API层面;二是工程能力,能够拆解复杂规范并在有限时间内产出可运行代码,这一经历在多数情况下足以过滤掉相当一部分停留在框架使用层面的候选人,如果你打算深入JVM方向,后续还可以继续推进GC和JIT的研究,这些领域的机会主要集中在基础设施和性能优化相关岗位上。
从零开始编写一个JVM,本质上是把计算机基础、编程语言设计和操作系统知识全部过一遍,最终的成果不一定需要达到生产级,但那个属于自己的java命令第一次启动时,你会对”虚拟机”这三个字产生完全不同于教科书的理解,按模块推进、以测试护航、以规范为准绳,这条路会在半年内走完。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/723599.html





