自己动手写 Java 虚拟机笔记 - 第二部分:搜索 Class 文件

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

前言

为什么要自己实现 JVM?

  • 兴趣驱动:想深入理解 "Write once, run anywhere" 的底层逻辑,跳出 "API 调用程序员" 的舒适区,亲手剖析 JVM 的核心原理。
  • 填补空白:目前网上关于 JVM 实现的资料中,针对 Mac 平台的实践较少,希望通过这份笔记给同类需求的开发者提供参考。

为什么选择 Go 语言?

  • 开发效率优势:相比 C/C++,Go 语言语法简洁、内存管理更友好,能降低开发门槛,让精力更聚焦于 JVM 核心功能的实现逻辑。
  • 学习双赢:借这个项目系统学习 Go 语言,在实践中掌握并发、指针、接口等特性。

参考资料

《自己动手写 Java 虚拟机》—— 张秀宏(核心参考书籍,推荐对 JVM 实现感兴趣的同学阅读)

开发环境

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

第二章 搜索 Class 文件

在 JVM 的执行流程中,第一步就是根据类名找到对应的 Class 文件并读取其内容。本章将实现 Class 文件的搜索逻辑,核心是解析类路径(classpath),并根据路径类型(目录、JAR 包等)加载 Class 文件。

一、解析 JRE 路径:获取基础类库位置

JVM 运行时依赖 JRE 中的基础类库(如java.lang.Object等核心类),因此需要先确定 JRE 的位置。我们通过命令行参数-Xjre接收用户指定的 JRE 路径,若未指定则自动查找系统默认 JRE。

1.1 扩展命令行参数解析

修改cmd.go,新增-Xjre参数的解析逻辑,用于接收 JRE 路径:

// 命令行选项和参数结构体
type Cmd struct {
    // ... 省略其他字段
    xJreOption string  // 存储-Xjre参数的值
}

// 解析命令行参数,赋值到Cmd结构体
func parseCmd() *Cmd {
    cmd := &Cmd{}
    flag.Usage = printUsage

    // 解析基础参数
    flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
    flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
    flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
    flag.StringVar(&cmd.cpOption, "cp", "", "classpath")
    
    // 新增:解析-Xjre参数
    flag.StringVar(&cmd.xJreOption, "Xjre", "", "path to jre")
    
    flag.Parse()
    // ... 省略后续逻辑
    return cmd
}

说明-Xjre是 JVM 的非标准选项(以-X开头),用于指定 JRE 的根目录,优先级高于系统默认 JRE 路径。

1.2 自动查找 JRE 路径

若用户未指定-Xjre,需要自动查找系统默认 JRE。在 MacOS 上,可通过以下逻辑实现(补充getJreDir函数):

// 获取JRE目录(简化版)
func getJreDir(jreOption string) string {
    // 1. 优先使用用户指定的-Xjre参数
    if jreOption != "" && exists(jreOption) {
        return jreOption
    }
    
    // 2. 查找当前Java_home下的JRE
    if javaHome := os.Getenv("JAVA_HOME"); javaHome != "" {
        jreDir := filepath.Join(javaHome, "jre")
        if exists(jreDir) {
            return jreDir
        }
    }
    
    // 3. 尝试当前目录下的jre文件夹
    if exists("./jre") {
        return "./jre"
    }
    
    // 4. 若都找不到,抛出错误
    panic("can not find jre directory")
}

// 辅助函数:判断路径是否存在
func exists(path string) bool {
    _, err := os.Stat(path)
    return err == nil || os.IsExist(err)
}

核心逻辑:JRE 路径的查找优先级为 “用户指定> JAVA_HOME 下的 jre > 当前目录 jre”,确保在大多数环境下能正确定位基础类库。

二、设计 Entry 接口:统一 Class 文件读取逻辑

Class 文件可能存在于目录、JAR 包、ZIP 包等不同位置,为了统一读取逻辑,我们定义Entry接口,抽象不同存储介质的 Class 文件读取行为。

2.1 Entry 接口定义

const pathListSeparator = string(os.PathListSeparator)  // 路径分隔符(Mac/Linux为:,Windows为;)

// Entry接口:定义Class文件读取规范
type Entry interface {
    // 读取类文件:返回类字节码、当前Entry实例、错误信息
    readClass(name string) ([]byte, Entry, error)
    // 字符串表示:返回当前Entry的路径描述
    String() string
}

接口作用:无论 Class 文件在目录还是 JAR 包中,都通过readClass方法读取,调用者无需关心底层存储细节。

2.2 Entry 的实现类

根据 Class 文件的存储位置,Entry有 4 种实现,覆盖所有常见场景:

实现类适用场景示例路径
DirEntry目录中的 Class 文件/Users/dev/classes
ZipEntryJAR/ZIP 包中的 Class 文件/Users/dev/lib/tools.jar
WildcardEntry通配符匹配的多个 JAR/ZIP 包/Users/dev/lib/*
CompositeEntry多个路径(用分隔符分隔)./classes:/Users/dev/lib/*

2.2.1 工厂方法:创建 Entry 实例

通过newEntry函数根据路径自动选择合适的实现类,简化调用:

// 根据路径创建对应的Entry实例
func newEntry(path string) Entry {
    // 1. 处理多路径(含分隔符)
    if strings.Contains(path, pathListSeparator) {
        return newCompositeEntry(path)
    }
    
    // 2. 处理通配符路径(以*结尾)
    if strings.HasSuffix(path, "*") {
        return newWildcardEntry(path)
    }
    
    // 3. 处理JAR/ZIP包
    if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
       strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
        return newZipEntry(path)
    }
    
    // 4. 默认视为目录
    return newDirEntry(path)
}

设计思路:通过工厂模式隐藏具体实现类的创建细节,调用者只需传入路径即可获得可用的Entry实例。

三、Classpath 类:管理完整类路径

JVM 的类路径由三部分组成:启动类路径(boot classpath)扩展类路径(ext classpath)用户类路径(user classpath)Classpath类负责管理这三部分路径的解析和使用。

3.1 Classpath 结构定义

package classpath

import (
    "os"
    "path/filepath"
)

// Classpath:管理完整的类路径
type Classpath struct {
    bootClasspath Entry  // 启动类路径(如JRE/lib下的类)
    extClasspath  Entry  // 扩展类路径(如JRE/lib/ext下的类)
    userClasspath Entry  // 用户类路径(-cp指定或默认当前目录)
}

3.2 解析类路径

3.2.1 解析启动类和扩展类路径

启动类路径包含 JVM 运行必需的核心类(如java.lang包下的类),扩展类路径包含系统扩展类,两者均位于 JRE 目录中:

// 解析启动类路径和扩展类路径
func (c *Classpath) parseBootAndExtClasspath(jreOption string) {
    jreDir := getJreDir(jreOption)  // 先获取JRE目录
    
    // 启动类路径:JRE/lib/*(匹配所有JAR包和类目录)
    jreLibPath := filepath.Join(jreDir, "lib", "*")
    c.bootClasspath = newWildcardEntry(jreLibPath)
    
    // 扩展类路径:JRE/lib/ext/*
    jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
    c.extClasspath = newWildcardEntry(jreExtPath)
}

说明JRE/lib/*会匹配该目录下所有 JAR 包(如rt.jar是核心类库),确保能加载基础类。

3.2.2 解析用户类路径

用户类路径由-cp参数指定(若未指定则默认当前目录),用于加载应用程序自身的类:

// 解析用户类路径
func (c *Classpath) parseUserClasspath(cpOption string) {
    if cpOption == "" {
        cpOption = "."  // 默认当前目录
    }
    c.userClasspath = newEntry(cpOption)
}

3.3 统一读取 Class 文件

Classpath提供ReadClass方法,按优先级(用户类路径 → 扩展类路径 → 启动类路径)查找并读取 Class 文件:

// 读取Class文件:按用户类路径→扩展类路径→启动类路径的顺序查找
func (c *Classpath) ReadClass(name string) ([]byte, Entry, error) {
    name = name + ".class"  // 补充.class后缀
    
    // 1. 先从用户类路径查找
    if data, entry, err := c.userClasspath.readClass(name); err == nil {
        return data, entry, nil
    }
    
    // 2. 再从扩展类路径查找
    if data, entry, err := c.extClasspath.readClass(name); err == nil {
        return data, entry, nil
    }
    
    // 3. 最后从启动类路径查找
    return c.bootClasspath.readClass(name)
}

优先级说明:用户类路径优先级最高,避免自定义类覆盖核心类;启动类路径优先级最低,确保核心类不被意外替换。

四、测试验证:读取 Class 文件

修改main.gostartJVM函数,验证 Classpath 是否能正确读取 Class 文件:

func startJVM(cmd *Cmd) {
    // 解析类路径
    cp := classpath.NewClasspath(cmd.xJreOption, cmd.cpOption)
    fmt.Printf("classpath: %v\n", cp)
    
    // 转换类名为路径格式(如java.lang.Object → java/lang/Object)
    classname := strings.ReplaceAll(cmd.class, ".", "/")
    
    // 读取Class文件
    data, entry, err := cp.ReadClass(classname)
    if err != nil {
        fmt.Printf("Could not find or load main class %s: %v\n", cmd.class, err)
        return
    }
    
    // 输出类的内容
        fmt.Printf("class data:%v\n", data)
}

测试步骤与结果

  1. 编译安装

    go install ./ch02/
  2. 执行测试:读取java.lang.Object类(JRE 核心类)

    ch02 java.lang.String
  3. 输出结果

ch02test.png

结果说明:成功从 JRE 的rt.jar中读取到java.lang.String类的字节码,验证了类路径解析和 Class 文件读取逻辑的正确性。

五、小结

本章实现了 JVM 搜索 Class 文件的核心逻辑,关键知识点:

  1. 类路径组成:启动类路径、扩展类路径、用户类路径的分层设计,确保类加载的优先级和安全性。
  2. Entry 接口:通过接口抽象不同存储介质的 Class 文件读取行为,简化上层调用。
  3. 路径解析:支持目录、JAR 包、通配符等多种路径格式,兼容 Java 的类路径规范。

下一章将基于本章的 Class 文件读取功能,实现 Class 文件的解析,提取常量池、类信息、方法等关键数据。

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

评论 (0)

取消