自己动手写 Java 虚拟机笔记 - 第三部分:解析 Class 文件核心结构

自己动手写 Java 虚拟机笔记 - 第三部分:解析 Class 文件核心结构

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

前言

在前两章中,我们搭建了 JVM 的命令行入口和类路径查找逻辑。本章将深入 Class 文件的二进制结构,实现从字节流到结构化数据的解析,这是 JVM 加载类的核心步骤。

参考资料

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

开发环境

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

第三章:解析 Class 文件

Class 文件是 Java 代码编译后的二进制文件,包含类的所有信息(类名、方法、字段、常量等)。JVM 通过解析 Class 文件才能理解并执行 Java 代码。本章将按 Class 文件的结构逐步实现解析逻辑。

一、Class 文件整体结构概览

Class 文件的结构严格遵循固定格式,通过不同长度的 “无符号整数”(u1/u2/u4 分别表示 1/2/4 字节无符号整数)描述数据。完整结构定义如下:

ClassFile {
   u4             magic;                  // 魔数,固定值 0xCAFEBABE(验证文件合法性)
   u2             minor_version;          // 次版本号(如 JDK 1.8 的 minor 为 0)
   u2             major_version;          // 主版本号(如 JDK 1.8 的 major 为 52)
   u2             constant_pool_count;    // 常量池大小(索引从 1 开始)
   cp_info        constant_pool[constant_pool_count-1]; // 常量池(存储类中所有常量)
   u2             access_flags;           // 类访问标志(如 public、final、abstract 等)
   u2             this_class;             // 类本身的常量池索引(指向类名)
   u2             super_class;            // 父类的常量池索引(指向父类名)
   u2             interfaces_count;       // 实现的接口数量
   u2             interfaces[interfaces_count]; // 接口列表(常量池索引数组)
   u2             fields_count;           // 字段数量
   field_info     fields[fields_count];   // 字段表(存储类的成员变量信息)
   u2             methods_count;          // 方法数量
   method_info    methods[methods_count]; // 方法表(存储类的方法信息)
   u2             attributes_count;       // 属性数量
   attribute_info attributes[attributes_count]; // 属性表(存储额外信息如代码、行号等)
}

核心目标:将上述二进制结构解析为 Go 语言中的结构体,便于后续 JVM 加载和执行。

二、实现字节流读取工具(ClassReader)

Class 文件本质是二进制字节流,我们需要一个工具类来按格式读取不同长度的数据(u1/u2/u4 等)。

1. ClassReader 结构体与核心方法

package classfile

import "encoding/binary"

// ClassReader 封装 Class 文件字节流的读取逻辑
type ClassReader struct {
    data []byte // 存储 Class 文件的二进制数据
}

// 读取 1 字节无符号整数(u1)
func (c *ClassReader) readUnit8() uint8 {
    val := c.data[0]       // 取当前第一个字节
    c.data = c.data[1:]    // 移动指针到下一个字节
    return val
}

// 读取 2 字节无符号整数(u2),大端字节序
func (c *ClassReader) readUnit16() uint16 {
    val := binary.BigEndian.Uint16(c.data) // 大端序解析 2 字节
    c.data = c.data[2:]                    // 移动指针
    return val
}

// 读取 4 字节无符号整数(u4),大端字节序
func (c *ClassReader) readUnit32() uint32 {
    val := binary.BigEndian.Uint32(c.data)
    c.data = c.data[4:]
    return val
}

// 读取 8 字节无符号整数(u8),大端字节序
func (c *ClassReader) readUnit64() uint64 {
    val := binary.BigEndian.Uint64(c.data)
    c.data = c.data[8:]
    return val
}

// 读取 u2 数组(先读长度,再读对应数量的 u2)
func (c *ClassReader) readUnit16s() []uint16 {
    length := c.readUnit16()              // 数组长度(u2)
    uint16s := make([]uint16, length)
    for i := range uint16s {
        uint16s[i] = c.readUnit16()       // 依次读取每个元素
    }
    return uint16s
}

// 读取指定长度的字节数组
func (c *ClassReader) readBytes(length uint32) []byte {
    bytes := c.data[:length]              // 截取指定长度
    c.data = c.data[length:]              // 移动指针
    return bytes
}

作用ClassReader 屏蔽了字节流操作的细节,让上层解析逻辑更简洁,只需调用对应方法即可按格式读取数据。

三、解析 Class 文件主结构(ClassFile)

ClassFile 结构体对应 Class 文件的整体结构,负责协调解析魔数、版本号、常量池、字段、方法等核心部分。

1. ClassFile 结构体定义

package classfile

import "fmt"

// ClassFile 存储解析后的 Class 文件信息
type ClassFile struct {
    magic        uint32          // 魔数(0xCAFEBABE)
    minorVersion uint16          // 次版本号
    majorVersion uint16          // 主版本号
    constantPool ConstantPool    // 常量池(核心数据结构)
    accessFlags  uint16          // 类访问标志
    thisClass    uint16          // 当前类的常量池索引
    superClass   uint16          // 父类的常量池索引
    interfaces   []uint16        // 接口索引列表
    fields       []*MemberInfo   // 字段列表
    methods      []*MemberInfo   // 方法列表
    attributes   []AttributeInfo // 属性列表
}

2. 核心解析逻辑(Parse 方法)

// Parse 从字节流解析 Class 文件
func Parse(classData []byte) (cf *ClassFile, err error) {
    // 捕获解析过程中的 panic(如格式错误),转为 error 返回
    defer func() {
        if r := recover(); r != nil {
            var ok bool
            err, ok = r.(error)
            if !ok {
                err = fmt.Errorf("%v", r) // 非 error 类型的 panic 转为 error
            }
        }
    }()

    cr := &ClassReader{classData} // 创建字节流读取器
    cf = &ClassFile{}
    cf.read(cr) // 开始解析
    return
}

// read 按 Class 文件结构依次解析各部分
func (c *ClassFile) read(reader *ClassReader) {
    c.readAndCheckMagic(reader)      // 验证魔数
    c.readAndCheckVersion(reader)    // 验证版本号
    c.constantPool = readConstantPool(reader) // 解析常量池
    c.accessFlags = reader.readUnit16()       // 读取访问标志
    c.thisClass = reader.readUnit16()         // 读取当前类索引
    c.superClass = reader.readUnit16()        // 读取父类索引
    c.interfaces = reader.readUnit16s()       // 读取接口列表
    c.fields = readMembers(reader, c.constantPool) // 解析字段
    c.methods = readMembers(reader, c.constantPool) // 解析方法
    c.attributes = readAttributes(reader, c.constantPool) // 解析属性
}

3. 关键验证步骤

  • 魔数验证:确保文件是合法的 Class 文件(固定为 0xCAFEBABE)。

    func (c *ClassFile) readAndCheckMagic(reader *ClassReader) {
      magic := reader.readUnit32()
      if magic != 0xCAFEBABE {
          panic("java.lang.ClassFormatError: invalid magic number")
      }
    }
  • 版本号验证:JVM 只支持特定版本的 Class 文件(如 JDK 1.8 支持 45.0 ~ 52.0 版本)。

    func (c *ClassFile) readAndCheckVersion(reader *ClassReader) {
      c.minorVersion = reader.readUnit16()
      c.majorVersion = reader.readUnit16()
      // 支持 JDK 1.0.2(45.0)到 JDK 1.8(52.0)的版本
      switch c.majorVersion {
      case 45:
          return // JDK 1.0.2
      case 46, 47, 48, 49, 50, 51, 52:
          if c.minorVersion == 0 {
              return // JDK 1.1 ~ 1.8
          }
      }
      panic("java.lang.UnsupportedClassVersionError")
    }

四、解析常量池(ConstantPool)

常量池是 Class 文件中最复杂的部分,存储类中所有常量(字符串、类名、方法名、字段名等),是解析其他结构的基础。

1. 常量池结构与类型

常量池由多个 cp_info 结构组成,每个结构以 tag 字段标识类型(共 17 种,如字符串、类引用、方法引用等)。核心类型如下:

tag 值常量类型作用
1ConstantUtf8Info存储 UTF-8 字符串(如类名、方法名)
7ConstantClassInfo类或接口的引用(指向 UTF-8 类名)
10ConstantMethodRefInfo方法引用(指向类和方法描述符)
12ConstantNameAndTypeInfo名称和类型描述符(字段 / 方法的元信息)

2. 常量池解析逻辑

package classfile

// ConstantPool 常量池(本质是常量接口数组)
type ConstantPool []ConstantInfo

// 读取常量池
func readConstantPool(reader *ClassReader) ConstantPool {
    cpCount := int(reader.readUnit16()) // 常量池大小(索引从 1 开始)
    cp := make([]ConstantInfo, cpCount)
    for i := 1; i < cpCount; i++ {
        cp[i] = readConstantInfo(reader, cp) // 解析单个常量
        // 注意:Long 和 Double 类型占 2 个索引位置
        switch cp[i].(type) {
        case *ConstantLongInfo, *ConstantDoubleInfo:
            i++ // 跳过下一个索引
        }
    }
    return cp
}

// 从常量池获取指定索引的常量
func (c ConstantPool) getConstantInfo(index uint16) ConstantInfo {
    if cpInfo := c[index]; cpInfo != nil {
        return cpInfo
    }
    panic("invalid constant pool index")
}

// 工具方法:通过索引获取类名(从 ConstantClassInfo 中解析)
func (c ConstantPool) getClassName(index uint16) string {
    classInfo := c.getConstantInfo(index).(*ConstantClassInfo)
    return c.getUtf8(classInfo.nameIndex) // 类名存储在 UTF-8 常量中
}

// 工具方法:通过索引获取 UTF-8 字符串
func (c ConstantPool) getUtf8(index uint16) string {
    utf8Info := c.getConstantInfo(index).(*ConstantUtf8Info)
    return utf8Info.str
}

关键设计:通过 ConstantInfo 接口抽象不同类型的常量,每种常量类型实现接口的 readInfo 方法,实现多态解析。

五、解析字段和方法(MemberInfo)

字段(Field)和方法(Method)的结构相似,都包含访问标志、名称索引、描述符索引和属性列表,因此可以用同一个 MemberInfo 结构体封装。

1. MemberInfo 结构体

package classfile

// MemberInfo 封装字段或方法的信息
type MemberInfo struct {
    cp              ConstantPool    // 常量池(用于解析名称和描述符)
    accessFlags     uint16          // 访问标志(如 public、private、static 等)
    nameIndex       uint16          // 名称的常量池索引(字段名/方法名)
    descriptorIndex uint16          // 描述符的常量池索引(类型信息)
    attributes      []AttributeInfo // 属性列表(如字段的常量值、方法的代码等)
}

// 读取字段或方法列表
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {
    memberCount := reader.readUnit16() // 成员数量
    members := make([]*MemberInfo, memberCount)
    for i := range members {
        members[i] = readMember(reader, cp) // 解析单个成员
    }
    return members
}

// 读取单个字段或方法
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {
    return &MemberInfo{
        cp:              cp,
        accessFlags:     reader.readUnit16(),
        nameIndex:       reader.readUnit16(),
        descriptorIndex: reader.readUnit16(),
        attributes:      readAttributes(reader, cp),
    }
}

// 工具方法:获取字段/方法名称
func (m *MemberInfo) Name() string {
    return m.cp.getUtf8(m.nameIndex)
}

// 工具方法:获取字段/方法描述符(如 "Ljava/lang/String;" 表示字符串类型)
func (m *MemberInfo) Descriptor() string {
    return m.cp.getUtf8(m.descriptorIndex)
}

示例:解析 java.lang.String 类的方法时,nameIndex 指向常量池中的 "length" 字符串,descriptorIndex 指向 "()I"(表示无参、返回 int 的方法)。

六、解析属性(AttributeInfo)

属性是 Class 文件中存储额外信息的结构,不同属性有不同的格式(如方法的字节码存储在 Code 属性中,行号映射存储在 LineNumberTable 属性中)。

1. 属性接口与解析逻辑

package classfile

// AttributeInfo 属性接口(所有属性类型都需实现此接口)
type AttributeInfo interface {
    readInfo(reader *ClassReader) // 读取属性具体内容
}

// 读取属性列表
func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {
    attributeCount := reader.readUnit16() // 属性数量
    attributes := make([]AttributeInfo, attributeCount)
    for i := range attributes {
        attributes[i] = readAttribute(reader, cp) // 解析单个属性
    }
    return attributes
}

// 读取单个属性(根据属性名创建对应类型的实例)
func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {
    attrNameIndex := reader.readUnit16()       // 属性名的常量池索引
    attrName := cp.getUtf8(attrNameIndex)      // 获取属性名(如 "Code"、"SourceFile")
    attrLen := reader.readUnit32()             // 属性长度(内容字节数)
    attribute := newAttribute(attrName, attrLen, cp) // 创建属性实例
    attribute.readInfo(reader)                 // 读取属性内容
    return attribute
}

// 根据属性名创建对应类型的实例
func newAttribute(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo {
    switch attrName {
    case "Code":
        return &CodeAttribute{cp: cp} // 方法的字节码属性
    case "ConstantValue":
        return &ConstantValueAttribute{} // 字段的常量值属性
    case "Exceptions":
        return &ExceptionAttribute{} // 方法抛出的异常属性
    case "LineNumberTable":
        return &LineNumberTableAttribute{} // 字节码与行号映射属性
    case "SourceFile":
        return &SourceFileAttribute{cp: cp} // 源文件名属性
    default:
        // 未实现的属性用 UnparsedAttribute 存储原始数据
        return &UnparsedAttribute{attrName, attrLen, nil}
    }
}

核心属性示例

  • Code 属性:存储方法的字节码指令、操作数栈大小、局部变量表大小等核心执行信息。
  • SourceFile 属性:记录类对应的源文件名(如 String.java)。

七、测试解析逻辑

1. 修改启动函数验证解析结果

main.go 中添加加载类并打印解析结果的逻辑:

func startJVM(cmd *Cmd) {
    // 解析类路径
    cp := classpath.Parse(cmd.xJreOption, cmd.cpOption)
    // 加载类(如 java.lang.String)
    classname := strings.ReplaceAll(cmd.class, ".", "/") // 转为类文件路径(如 "java/lang/String")
    class := loadClass(classname, cp)
    printClass(class) // 打印解析结果
}

// 打印类的核心信息
func printClass(cf *classfile.ClassFile) {
    fmt.Printf("version: %v.%v\n", cf.MajorVersion(), cf.MinorVersion())
    fmt.Printf("constants count: *s", len(cf.ConstantPool()))
    fmt.Printf("access flag: 0x%x\n", cf.AccessFlags())
    fmt.Printf("this class: %s\n", cf.ClassName())
    fmt.Printf("super class: %s\n", cf.SuperClassName())
    fmt.Printf("interfaces name: %s\n", cf.InterfaceNames())
    fmt.Printf("filed count: %s\n", len(cf.Fields()))
    for _, field := range cf.Fields() {
        fmt.Printf("%s\n", field.Name())
    }
    fmt.Printf("method count: %s\n", len(cf.Methods()))
    for _, method := range cf.Methods() {
        fmt.Printf("%s\n", method.Name())
    }
}

2. 执行测试并验证结果

  • 编译命令:go install ./ch03/
  • 执行命令:ch03 java.lang.String
  • 预期输出:打印 java.lang.String 类的版本号、类名、父类名(java.lang.Object)、字段(如 value)、方法(如 lengthequals)等信息。

ch03test.png

本章小结

本章完成了 Class 文件的核心解析逻辑,重点包括:

  1. ClassReader 封装字节流读取,按格式解析 u1/u2/u4 等数据;
  2. 实现 ClassFile 结构体,按固定结构解析魔数、版本号、常量池等核心部分;
  3. 通过接口抽象常量池和属性的多态解析,适配不同类型的常量和属性;
  4. MemberInfo 统一封装字段和方法的解析逻辑。

下一章将基于解析后的 Class 文件信息,实现运行时数据区,进一步理解JVM内部的奥秘。

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

评论 (0)

取消