自己动手写 Java 虚拟机笔记 - 第五部分:指令集与解释器实现

自己动手写 Java 虚拟机笔记 - 第五部分:指令集与解释器实现

Luca Ju
2025-06-20 / 0 评论 / 5 阅读 / 正在检测是否收录...

前言

在前一章中,我们实现了 JVM 运行时数据区(线程、栈帧、局部变量表、操作数栈等),为字节码执行提供了 “内存环境”。本章将聚焦 JVM 的指令集和解释器—— 指令集是字节码的 “操作命令”,解释器则负责将这些命令翻译成具体操作并执行,这是 JVM 执行程序的核心逻辑。

参考资料

《自己动手写 Java 虚拟机》—— 张秀宏

开发环境

工具 / 环境版本说明
操作系统MacOS 15.5基于 Intel/Apple Silicon 均可
JDK1.8用于字节码分析和测试
Go 语言1.23.10项目开发主语言

第五章:指令集与解释器核心实现

JVM 通过字节码指令控制程序执行,每条指令对应特定的操作(如变量加载、算术运算、方法调用等)。解释器的作用是读取字节码,解析出指令并执行对应的操作,最终完成方法的逻辑。本章将实现常用指令集和基础解释器。

一、指令集基础:常量池与核心结构回顾

在解析指令前,需先回顾 Class 文件中与指令执行相关的核心结构,这些结构是指令操作的 “元数据” 来源。

1. 常量池 Tag 对应关系

常量池存储了指令执行所需的常量(字符串、类名、方法名等),每条常量通过 tag 字段标识类型。以下是核心常量类型的对应关系:

Tag 值(十进制)Tag 值(十六进制)助记符说明
10x01CONSTANT_Utf8UTF-8 编码的字符串常量(如类名、方法名)
30x03CONSTANT_Integer整型常量
40x04CONSTANT_Float浮点型常量
50x05CONSTANT_Long长整型常量(占两个常量池条目)
60x06CONSTANT_Double双精度浮点型常量(占两个常量池条目)
70x07CONSTANT_Class类或接口的符号引用(指向类名)
100x0aCONSTANT_Methodref类方法的符号引用(指向类和方法描述符)
120x0cCONSTANT_NameAndType字段 / 方法的名称和描述符引用

作用:指令执行时需通过常量池索引获取具体数据(如调用方法时通过 CONSTANT_Methodref 找到方法地址)。

2. 核心结构定义

指令的操作依赖 Class 文件中的字段表、方法表和属性表,以下是关键结构回顾:

  • 方法表(method_info):存储方法的访问标志、名称、描述符和属性(核心是 Code 属性,包含字节码)。

    method_info {
      u2 access_flags;       // 方法访问标志(如 public、static)
      u2 name_index;         // 方法名的常量池索引
      u2 descriptor_index;   // 方法描述符的常量池索引(如 "(I)V" 表示入参 int、返回 void)
      u2 attributes_count;   // 属性数量
      attribute_info attributes[attributes_count]; // 包含 Code 属性
    }
  • Code 属性:方法的核心属性,存储字节码指令、操作数栈大小、局部变量表大小等。

    Code_attribute {
      u2 attribute_name_index; // 指向 "Code" 字符串
      u4 attribute_length;
      u2 max_stack;            // 操作数栈最大深度
      u2 max_locals;           // 局部变量表大小
      u4 code_length;          // 字节码长度
      u1 code[code_length];    // 字节码指令数组(核心执行内容)
      // 省略异常表和子属性...
    }

作用:解释器通过 Code 属性获取字节码指令,结合 max_stackmax_locals 初始化栈帧。

二、指令集分类与实现

JVM 指令集包含数百条指令,按功能可分为常量加载、变量操作、算术运算、控制转移等类型。以下实现核心指令的关键逻辑。

1. 基础指令(无操作 / 常量加载)

  • nop 指令:无操作指令,用于字节码对齐(不执行任何操作)。

    // Nop 无操作指令
    type Nop struct {
      base.NoOperandsInstruction // 无操作数指令基类
    }
    func (n *Nop) Execute(frame *rtda.Frame) {
      // 空实现:仅占位,无实际操作
    }
  • const 指令:将常量推入操作数栈(如 aconst_null 推入 null 引用,iconst_0 推入 int 0)。

    // ACONST_NULL 推送 null 引用到操作数栈
    type ACONST_NULL struct {
      base.NoOperandsInstruction
    }
    func (a *ACONST_NULL) Execute(frame *rtda.Frame) {
      frame.OperandStack().PushRef(nil) // 操作数栈推送 null
    }
    
    // ICONST_0 推送 int 0 到操作数栈
    type ICONST_0 struct {
      base.NoOperandsInstruction
    }
    func (i *ICONST_0) Execute(frame *rtda.Frame) {
      frame.OperandStack().PushInt(0) // 操作数栈推送 int 0
    }

2. 常量推送指令(bipush/sipush)

用于将小范围整数推入操作数栈(bipush 支持 8 位整数,sipush 支持 16 位整数)。

// BIPUSH 推送 8 位整数到操作数栈
type BIPUSH struct {
   val int8 // 指令自带的 8 位常量值
}

// 从字节码中读取操作数(8 位整数)
func (b *BIPUSH) FetchOperands(reader *base.BytecodeReader) {
   b.val = int8(reader.ReadInt8())
}

// 执行:推送常量到操作数栈
func (b *BIPUSH) Execute(frame *rtda.Frame) {
   frame.OperandStack().PushInt(int32(b.val))
}

// SIPUSH 推送 16 位整数到操作数栈(逻辑类似,略)
type SIPUSH struct {
   val int16
}

3. 局部变量操作指令(加载 / 存储)

  • 加载指令(iload/iload_0):从局部变量表加载 int 类型到操作数栈(iload_0iload 0 的简写,优化性能)。

    // ILOAD 从局部变量表加载 int(通过索引指定位置)
    type ILOAD struct {
      base.Index8Instruction // 包含 8 位索引字段
    }
    func (i *ILOAD) Execute(frame *rtda.Frame) {
      // 从局部变量表 index 位置加载 int,推入操作数栈
      index := i.Index
      val := frame.LocalVars().GetInt(index)
      frame.OperandStack().PushInt(val)
    }
    
    // ILOAD_0 从局部变量表 index 0 加载 int(简写指令,无操作数)
    type ILOAD_0 struct {
      base.NoOperandsInstruction
    }
    func (i *ILOAD_0) Execute(frame *rtda.Frame) {
      val := frame.LocalVars().GetInt(0) // 固定 index 0
      frame.OperandStack().PushInt(val)
    }
  • 存储指令(istore/istore_0):从操作数栈弹出 int 类型到局部变量表(逻辑与加载指令相反)。

    // ISTORE 存储 int 到局部变量表
    type ISTORE struct {
      base.Index8Instruction
    }
    func (i *ISTORE) Execute(frame *rtda.Frame) {
      val := frame.OperandStack().PopInt() // 从操作数栈弹出
      frame.LocalVars().SetInt(i.Index, val) // 存入局部变量表 index 位置
    }

4. 栈操作指令(pop/dup/swap)

操作数栈的元素管理指令,用于调整栈中数据顺序。

  • pop 指令:弹出操作数栈顶元素(用于清理不需要的数据)。

    type POP struct {
      base.NoOperandsInstruction
    }
    func (p *POP) Execute(frame *rtda.Frame) {
      frame.OperandStack().PopSlot() // 弹出栈顶槽位(Slot)
    }
  • swap 指令:交换操作数栈顶两个元素的位置(用于调整计算顺序)。

    // SWAP 交换栈顶两个元素(假设为 int 类型)
    type SWAP struct {
      base.NoOperandsInstruction
    }
    func (s *SWAP) Execute(frame *rtda.Frame) {
      stack := frame.OperandStack()
      slot1 := stack.PopSlot() // 弹出栈顶第一个元素
      slot2 := stack.PopSlot() // 弹出栈顶第二个元素
      stack.PushSlot(slot1)    // 先推回第一个元素
      stack.PushSlot(slot2)    // 再推回第二个元素(完成交换)
    }

5. 算术运算指令(iadd/ladd 等)

对操作数栈中的元素执行算术运算,结果推回栈顶。

// IADD 对操作数栈顶两个 int 相加
type IADD struct {
   base.NoOperandsInstruction
}
func (i *IADD) Execute(frame *rtda.Frame) {
   stack := frame.OperandStack()
   v2 := stack.PopInt() // 弹出第二个操作数
   v1 := stack.PopInt() // 弹出第一个操作数
   result := v1 + v2    // 计算
   stack.PushInt(result) // 结果推回栈顶
}

// LADD 对操作数栈顶两个 long 相加(逻辑类似,略)
type LADD struct {
   base.NoOperandsInstruction
}

6. 控制转移指令(if/loop/tableswitch)

改变程序执行流程,实现分支、循环等逻辑。

  • if_acmpeq 指令:比较两个引用是否相等,相等则跳转。

    // IF_ACMPEQ 若两个引用相等则跳转
    type IF_ACMPEQ struct {
      base.BranchInstruction // 包含跳转偏移量 Offset
    }
    func (i *IF_ACMPEQ) Execute(frame *rtda.Frame) {
      stack := frame.OperandStack()
      v2 := stack.PopRef() // 弹出第二个引用
      v1 := stack.PopRef() // 弹出第一个引用
      if v1 == v2 {
          base.Branch(frame, i.Offset) // 相等则跳转到 Offset 位置
      }
      // 不相等则继续执行下一条指令
    }
  • tableswitch 指令:用于 switch-case 语句的连续整数匹配(高效跳转)。

    // TABLE_SWITCH 按整数索引跳转(适用于连续 case 值)
    type TABLE_SWITCH struct {
      defaultOffset int32   // 默认跳转偏移量
      low           int32   // case 最小值
      high          int32   // case 最大值
      jumpOffsets   []int32 // 每个 case 对应的跳转偏移量
    }
    
    // 执行:根据栈顶整数选择跳转目标
    func (t *TABLE_SWITCH) Execute(frame *rtda.Frame) {
      stack := frame.OperandStack()
      i := stack.PopInt() // 弹出 switch 的条件值
      // 若值在 [low, high] 范围内,则跳转到对应偏移量
      if i >= t.low && i <= t.high {
          index := i - t.low
          base.Branch(frame, int(t.jumpOffsets[index]))
      } else {
          base.Branch(frame, int(t.defaultOffset)) // 否则走默认分支
      }
    }

三、解释器实现

解释器是连接字节码和运行时数据区的核心组件,负责:读取字节码指令→解析指令→执行指令操作→推进程序计数器。

1. 指令工厂:根据 opcode 创建指令对象

JVM 指令通过 opcode(操作码,1 字节) 区分类型,工厂类根据 opcode 生成对应指令实例。

// NewInstruction 根据 opcode 创建指令对象
func NewInstruction(opcode byte) base.Instruction {
   switch opcode {
   case 0x00:  // nop 指令 opcode
       return &Nop{}
   case 0x01:  // aconst_null 指令 opcode
       return &ACONST_NULL{}
   case 0x10:  // bipush 指令 opcode
       return &BIPUSH{}
   case 0x15:  // iload 指令 opcode
       return &ILOAD{}
   case 0x60:  // iadd 指令 opcode
       return &IADD{}
   case 0xa5:  // if_acmpeq 指令 opcode
       return &IF_ACMPEQ{}
   // 省略其他指令...
   default:
       panic(fmt.Sprintf("未实现的指令 opcode: 0x%x", opcode))
   }
}

2. 核心解释逻辑(interpret 方法)

解释器的主流程:初始化运行时环境→循环读取字节码→执行指令→处理异常。

// interpret 解释执行方法的字节码
func interpret(methodInfo *classfile.MemberInfo) {
   // 1. 从方法信息中获取 Code 属性(包含字节码和栈/变量表大小)
   codeAttr := methodInfo.CodeAttribute()
   maxLocals := codeAttr.MaxLocals()  // 局部变量表大小
   maxStack := codeAttr.MaxStack()    // 操作数栈大小
   bytecode := codeAttr.Code()        // 字节码指令数组

   // 2. 初始化运行时环境(线程、栈帧)
   thread := rtda.NewThread()         // 创建线程
   frame := thread.NewFrame(uint(maxLocals), uint(maxStack)) // 创建栈帧
   thread.PushFrame(frame)            // 栈帧入栈

   // 3. 异常捕获:确保执行出错时打印信息
   defer catchErr(frame)

   // 4. 循环执行字节码指令
   loop(thread, bytecode)
}

// loop 循环读取并执行指令
func loop(thread *rtda.Thread, bytecode []byte) {
   frame := thread.CurrentFrame()
   reader := &base.BytecodeReader{} // 字节码读取器

   for {
       // 获取当前程序计数器(指令地址)
       pc := frame.NextPC()
       thread.SetPC(pc)

       // 读取 opcode(1 字节)
       reader.Reset(bytecode, pc)
       opcode := reader.ReadUint8()

       // 创建指令对象并读取操作数
       inst := NewInstruction(opcode)
       inst.FetchOperands(reader)

       // 更新程序计数器(指向 next 指令)
       frame.SetNextPC(reader.PC())

       // 执行指令
       fmt.Printf("pc: %d, opcode: 0x%x, inst: %T\n", pc, opcode, inst)
       inst.Execute(frame)
   }
}

核心逻辑:通过程序计数器(PC)定位当前指令,工厂类创建指令实例后执行,执行完成后更新 PC 指向下一步指令,形成循环。

四、测试:执行 1-100 求和逻辑

为验证指令集和解释器的正确性,我们通过一个简单的 Java 程序(1-100 求和)进行测试。

1. 测试代码与字节码分析

Java 测试类:

public class GuessTest {
   public static void main(String[] args) {
       int result = 0;          // 局部变量表 index 1(index 0 为 this)
       for (int i = 1; i <= 100; i++) { // i 在局部变量表 index 2
           result += i;         // 累加逻辑:result = result + i
       }
   }
}

字节码指令(循环累加部分):

// 简化的字节码指令(核心逻辑)
0: iconst_0         // 推送 0 到操作数栈
1: istore_1         // 弹出 0 存入局部变量表 index 1(result = 0)
2: iconst_1         // 推送 1 到操作数栈
3: istore_2         // 弹出 1 存入局部变量表 index 2(i = 1)
4: iload_2          // 加载 i 到操作数栈
5: bipush 100       // 推送 100 到操作数栈
7: if_icmpgt 21     // 若 i > 100 则跳转到 21(退出循环)
10: iload_1         // 加载 result 到操作数栈
11: iload_2         // 加载 i 到操作数栈
12: iadd            // result + i,结果推回栈顶
13: istore_1        // 弹出结果存入 result(更新 result)
14: iinc 2, 1       // i += 1(局部变量表 index 2 自增 1)
17: goto 4          // 跳转到 4(继续循环)
21: return          // 方法返回(未实现,测试中会报错)

手动解析二进制字节码

ch05_mannualanalyze.png

2. 测试结果与验证

执行测试命令:go install ./ch05/ && ch05,尽管因未实现 return 指令报错,但局部变量表中 result 的值已正确计算为 5050(1-100 求和结果)。

ch05_test.png

结论:核心指令(iconst/istore/iadd/iinc/goto)执行正确,验证了解释器和指令集的有效性。

本章小结

本章实现了 JVM 指令集的核心逻辑和解释器,重点包括:

  1. 指令集分类实现:常量加载、局部变量操作、算术运算、控制转移等指令,覆盖基础执行逻辑。
  2. 解释器核心流程:通过指令工厂创建指令实例,循环读取字节码、执行指令并更新程序计数器。
  3. 测试验证:通过 1-100 求和案例验证指令执行正确性,局部变量表结果符合预期。

下一章将实现类和对象、体会类加载执行的过程。

源码地址:https://github.com/Jucunqi/jvmgo.git
0

评论 (0)

取消