在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

虚拟机

语言虚拟机循环读取字节码并执行状态操作。ADD 是弹出两值、压入结果,不是重新读取源码里的加号。对于 1 + 2 * 3,代码生成先写 MUL,因为执行乘法之前,2 和 3 必须已经入栈。

LOAD_CONST 放入一个值,MUL 取出栈顶两个值再放回乘积。源码先出现 + 不影响执行,VM 只按指令列表行动。理解这点,才能分清正常得到 7 与栈下溢。

语言虚拟机如何工作

左侧列表是程序,上节按 AST 子节点在前、运算符在后的顺序生成,本节执行。CPython、JVM 等语言运行时及教学 VM 都可执行指令流,而非直接按源码字符顺序运算。本课指令集为:

指令操作
LOAD_CONST v压入 v
LOAD_NAME x从局部变量 x 读取并压栈
STORE_NAME x弹出一值写入局部变量 x
ADD / MUL / SUB先弹 b,再弹 a,压入 a ⊙ b
JUMP dest将 PC 设为 dest
JZ dest弹出一值,若为 0 则 PC 设为 dest
HALT返回栈中剩余结果

循环顺序是 op = code[pc]; pc += 1; dispatch(op)。程序里的循环在此不是特殊语法,而是写回较小 PC 的跳转。

操作数顺序不能反。SUB 先弹 b、再弹 a,计算 a - b,所以 LOAD n, LOAD 1, SUB 是 n - 1。若反过来计算就变成 1 - n,倒计时语义被破坏。

JIT 标签页增加热点检测:向后跳转达到阈值后,停止逐条取这些字节码,改运行编译后的循环体。字节码相同,执行方式改变。

虚拟机逐步演示

默认解释执行 1 + 2 * 3,列表为 LOAD_CONST 1, LOAD_CONST 2, LOAD_CONST 3, MUL, ADD, HALT,初始栈空。

  1. 展示六行程序,PC=0,栈无值。
  2. LOAD_CONST 1 压入 1,栈高 1。
  3. LOAD_CONST 2 压入 2,栈高 2。
  4. LOAD_CONST 3 压入 3,栈高 3。所有叶值已入栈,还未执行运算符。
  5. MUL 弹出 3、2,压入 6,栈高从 3 降到 2。乘法在 AST 中更深,所以代码先生成、这里先执行它。
  6. ADD 弹出 6、1,压入 7。它用的是乘积 6,不是原来的 2。
  7. HALT 停止,栈中一个值。
  8. 结果为 7,共执行六条字节码,包含 HALT,MUL 先于 ADD。

切换 (1 + 2) * 3,列表中 ADD 先于 MUL,同一 VM 先得到 3、再得到 9。ADD first 错误预设把运算符放在前面,空栈执行 ADD 抛出 Underflow,后面的加载不执行。

JIT 默认倒计时 n=3、阈值 2:解释运行两轮,编译,再用一个原生节拍完成剩余 n = n - 1,不再取循环字节码。冷循环阈值 5,热点停在 2/5,不编译,也不出现 Native。JIT 是触发策略,不保证每次都编译。

代码面板只亮实际分支:正常 ADD 高亮弹栈、压栈,不亮 Underflow;下溢时才亮 raise。解释器的 JUMP 不亮其代码片段中不存在的 compile_loop。JIT 代码里的 THRESHOLD 与滑块同步,例如冷循环显示 5。

指令顺序、时间与空间

代码列表来自树,栈机需要操作数先到位,所以生成器用后序。(1 + 2) * 3 的加法更深,先算 ADD,栈高峰值为 2;默认峰值为 3。解析器构树、生成器遍历、VM 执行是三个阶段。

在本课小值模型中,每条操作码 O(1),k 条直线字节码需要 k 次取指,n 轮计数循环在解释阶段需要 O(n) 次取指。JIT 后剩余 r 轮在画布合成一个原生节拍,免去字节码取指与分派,但算术工作仍为 Θ(r),并未直接跳到结果。栈空间取决于峰值高度,不是源码长度。

边界情况

  • 初始栈空时隐藏 TOP,不在空带旁打印过时的 stack=0 结果样式。
  • ADD、MUL、SUB 需要两个值,不足就抛异常并停止。余下列表保留但不运行;停止帧不显示容易被当作结果的右侧状态头。
  • x + 2 * 3 且 x=10 时,LOAD_NAME 从局部变量面板压入 10;乘法得到 6,最后加成 16,名称加载不是常量加载。
  • 倒计时在 JUMP 时栈为空,循环各轮保持栈平衡,持久状态是局部变量 n。
  • 冷循环两轮、热点 2、阈值 5,终态为 still interpreting · hot 2/5,没有 Native。
  • HALT 是实际最后一行,VM 不靠越过列表末端停止。本节在上节五条指令后加 HALT,默认六条中五条改变栈。
  • PC 在分派前加一,JUMP 再覆盖它。若跳转后再加一,JUMP 2 会错误落到 3;CPU 课的 JNZ 也需区分这两步。

常见错误

  • 把 ADD 当源码中的 + 直接执行。解析已经决定树,ADD 按字节码位置完成两次弹出一次压入。
  • 运算符先生成,触发 ADD first 的栈下溢。寄存器 VM 是另一种机器模型。
  • 把运行栈当 IR。上课列表是生成的程序,这里的格子是该程序使用的运行数据。
  • 以为 JIT 省掉算术。原生循环仍逐轮执行 n = n - 1,只是省取指;直接令 n=0 会变成另一种优化,不能伪装成本课执行。
  • JUMP 之后再增加 PC,跳过真正目的地址。

模型范围

这是教学栈指令集,没有调用帧、堆、对象、垃圾回收或类型系统。Native 条带显示融合后的源级循环 n = n - 1 / goto loop,不是 x86,也没有反优化。LuaJIT、V8、Dalvik 等相关执行体系中的寄存器式字节码,具有不同操作数模型;KVM 和第一类、第二类管理程序属于系统虚拟化,不能与本课语言 VM 混为一类。

本课保留的机制是取指、执行栈操作、进入下一指令;循环用跳转改写 PC,JIT 在解释流程上增加热点检查并切换执行体。