前言
为什么要自己实现 JVM?
- 兴趣驱动:想深入理解 "Write once, run anywhere" 的底层逻辑,跳出 "API 调用程序员" 的舒适区,亲手剖析 JVM 的核心原理。
- 填补空白:目前网上关于 JVM 实现的资料中,针对 Mac 平台的实践较少,希望通过这份笔记给同类需求的开发者提供参考。
为什么选择 Go 语言?
- 开发效率优势:相比 C/C++,Go 语言语法简洁、内存管理更友好,能降低开发门槛,让精力更聚焦于 JVM 核心功能的实现逻辑。
- 学习双赢:借这个项目系统学习 Go 语言,在实践中掌握并发、指针、接口等特性。
参考资料
《自己动手写 Java 虚拟机》—— 张秀宏(核心参考书籍,推荐对 JVM 实现感兴趣的同学阅读)
开发环境
| 工具 / 环境 | 版本 | 说明 |
|---|---|---|
| 操作系统 | MacOS 15.5 | 基于 Intel/Apple Silicon 均可 |
| JDK | 1.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 |
ZipEntry | JAR/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.go的startJVM函数,验证 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)
}测试步骤与结果
编译安装:
go install ./ch02/执行测试:读取
java.lang.Object类(JRE 核心类)ch02 java.lang.String- 输出结果:

结果说明:成功从 JRE 的rt.jar中读取到java.lang.String类的字节码,验证了类路径解析和 Class 文件读取逻辑的正确性。
五、小结
本章实现了 JVM 搜索 Class 文件的核心逻辑,关键知识点:
- 类路径组成:启动类路径、扩展类路径、用户类路径的分层设计,确保类加载的优先级和安全性。
- Entry 接口:通过接口抽象不同存储介质的 Class 文件读取行为,简化上层调用。
- 路径解析:支持目录、JAR 包、通配符等多种路径格式,兼容 Java 的类路径规范。
下一章将基于本章的 Class 文件读取功能,实现 Class 文件的解析,提取常量池、类信息、方法等关键数据。
源码地址:https://github.com/Jucunqi/jvmgo.git
评论 (0)