【DIY系列:Java虚拟机】第10篇:Classpath 整合——让 JVM 找到所有 class 文件 上一篇【第09篇】CompositeEntry 与 WildcardEntry——组合与通配符的魔法下一篇【第11篇】class 文件结构总览——JVM 的通用语摘要前面三篇做完了 Entry 接口和四种实现这一篇把它们组装成完整的Classpath。你会看到Classpath 如何用三个字段承载三大类路径、Parse()如何作为统一入口、getJreDir()如何用三级探测策略找到 JRE 目录-Xjre→./jre→JAVA_HOME、以及最重要的ReadClass()如何按启动 → 扩展 → 用户的顺序搜索。最后我们把 ch02 完整跑起来——第一次从 JDK 的 rt.jar 里读出java.lang.Object的字节码。虽然打印出来是一堆乱码般的数字但这是我们的 JVM 第一次真正摸到 Java 类的实体。一、Classpath 结构体三大路径的容器Classpath 的职责是统筹三大类路径对外提供统一的查找接口。// ch02/classpath/classpath.gopackageclasspathimport(ospath/filepath)// Classpath 表示完整的类路径由三部分组成typeClasspathstruct{bootClasspath Entry// 启动类路径jre/lib/*extClasspath Entry// 扩展类路径jre/lib/ext/*userClasspath Entry// 用户类路径-cp 指定的或当前目录}三个字段类型都是Entry接口——这正是组合模式的价值不管每种路径内部有多复杂可能是单个目录、可能是通配符展开的一堆 jar对 Classpath 来说都只是一个 Entry。【Classpath 的三层结构】 ┌────────────────────────────────────────────────────────┐ │ Classpath │ │ │ │ bootClasspath extClasspath userClasspath │ │ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │ │WildcardEntry │ │WildcardEntry │ │ newEntry │ │ │ │jre/lib/* │ │jre/lib/ext/* │ │ (-cp 值) │ │ │ └──────┬───────┘ └──────┬───────┘ └─────┬──────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ rt.jar, resources.jar, charsets.jar, ... │ │ │ │ → CompositeEntry([ZipEntry, ZipEntry, ...]) │ │ │ └──────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────┘ 搜索顺序bootClasspath → extClasspath → userClasspath 找到第一个就返回二、Parse()统一解析入口// Parse 解析类路径jreOption 是 -Xjre 选项cpOption 是 -cp 选项funcParse(jreOption,cpOptionstring)*Classpath{cp:Classpath{}cp.parseBootAndExtClasspath(jreOption)// 解析启动类路径和扩展类路径cp.parseUserClasspath(cpOption)// 解析用户类路径returncp}简洁明了——两件事各交给一个方法。调用方式在 startJVM 里cp:classpath.Parse(cmd.XjreOption,cmd.cpOption)三、getJreDir()JRE 目录的三级探测这是本章最有工程味的一段代码。问题启动类路径和扩展类路径都在 JRE 目录下那JRE 目录在哪儿不同机器上的 JDK 安装位置千差万别硬编码肯定不行。原书用了三级探测策略funcgetJreDir(jreOptionstring)string{// 第一级用户通过 -Xjre 显式指定ifjreOption!exists(jreOption){returnjreOption}// 第二级当前目录下有 jre 子目录ifexists(./jre){return./jre}// 第三级JAVA_HOME 环境变量ifjh:os.Getenv(JAVA_HOME);jh!{returnfilepath.Join(jh,jre)}// 都找不到只能报错panic(Can not find jre folder!)}【getJreDir 三级探测流程】 开始查找 JRE 目录 │ ▼ ┌──────────────────────────────┐ │ ① -Xjre 选项有值且目录存在 │──是──► 返回该路径 └──────────┬───────────────────┘ 优先级最高显式指定 │ 否 ▼ ┌──────────────────────────────┐ │ ② 当前目录下有 ./jre 吗 │──是──► 返回 ./jre └──────────┬───────────────────┘ 绿色部署场景 │ 否 ▼ ┌──────────────────────────────┐ │ ③ JAVA_HOME 环境变量设置了 │──是──► 返回 $JAVA_HOME/jre └──────────┬───────────────────┘ 最常见场景 │ 否 ▼ panic(Can not find jre folder!)级别来源适用场景优先级①-Xjre选项手动运行 jvmgo 时显式指定最高②./jre目录绿色版部署JRE 跟着程序走中③JAVA_HOME环境变量标准安装最省心低—都找不到panic快速失败—重点注意第三级用的是filepath.Join(jh, jre)——因为JAVA_HOME指向的是JDK根目录而 JRE 在它的jre子目录里。JDK 8 的目录结构是$JAVA_HOME/jre/lib/rt.jar。exists()判断路径是否存在funcexists(pathstring)bool{if_,err:os.Stat(path);err!nil{ifos.IsNotExist(err){returnfalse// 路径不存在}}returntrue// 路径存在或者出了别的错乐观认为存在}os.Stat()获取文件/目录的元信息。如果路径不存在会返回一个 error用os.IsNotExist(err)判断是不是不存在导致的。细节这个函数在遇到其他错误比如权限不足时返回true。这是乐观策略——出错时先当作存在让后续操作去发现真正的问题。生产代码里可能要处理得更细致但学习项目这样足够了。四、parseBootAndExtClasspath()启动与扩展类路径func(self*Classpath)parseBootAndExtClasspath(jreOptionstring){jreDir:getJreDir(jreOption)// 启动类路径jre/lib/*jreLibPath:filepath.Join(jreDir,lib,*)self.bootClasspathnewWildcardEntry(jreLibPath)// 扩展类路径jre/lib/ext/*jreExtPath:filepath.Join(jreDir,lib,ext,*)self.extClasspathnewWildcardEntry(jreExtPath)}两个都用newWildcardEntry——因为jre/lib/下有一堆 jarrt.jar、resources.jar、charsets.jar…用通配符一次全收。【jre/lib 目录的实际内容JDK 8】 $JAVA_HOME/jre/lib/ ├── rt.jar ← 核心类库约 60MB2万 类 ├── resources.jar ← 资源文件本地化、字体等 ├── charsets.jar ← 字符集 ├── jce.jar ← 加密扩展 ├── jsse.jar ← SSL/TLS ├── ext/ ← 这是扩展类路径 │ ├── access-bridge-64.jar │ ├── cldrdata.jar │ ├── dnsns.jar │ └── ... └── ... jre/lib/* → WildcardEntry 会收集 rt.jar, resources.jar, charsets.jar ... jre/lib/ext/* → WildcardEntry 会收集 ext/ 下的所有 jar重点这里用filepath.Join(jreDir, lib, *)而不是字符串拼接——Join会自动处理不同系统的路径分隔符。最后拼出来的路径末尾带*正好符合 WildcardEntry 的要求。五、parseUserClasspath()用户类路径func(self*Classpath)parseUserClasspath(cpOptionstring){// 没指定 -cp 就用当前目录ifcpOption{cpOption.}self.userClasspathnewEntry(cpOption)}简单到没什么好说的——没指定就用.当前目录这跟真实 java 命令的行为一致。注意这里用的是newEntry()工厂函数而不是某个具体的实现。因为用户类路径可能是目录、jar、通配符、或组合让工厂函数去判断。六、ReadClass()按序搜索这是 Classpath 对外最核心的方法func(self*Classpath)ReadClass(classNamestring)([]byte,Entry,error){classNameclassName.class// 补上 .class 后缀// 1. 先查启动类路径ifdata,entry,err:self.bootClasspath.readClass(className);errnil{returndata,entry,err}// 2. 再查扩展类路径ifdata,entry,err:self.extClasspath.readClass(className);errnil{returndata,entry,err}// 3. 最后查用户类路径returnself.userClasspath.readClass(className)}【ReadClass 搜索流程】 ReadClass(java/lang/Object) │ │ className .class ▼ java/lang/Object.class │ ▼ ┌─────────────────────────────┐ │ ① bootClasspath.readClass() │ 在 rt.jar 等核心 jar 里找 └──────────┬──────────────────┘ │ 找到 ├─ 是 ──► 返回 (data, entry, nil) ✅ 结束 │ ▼ 未找到 ┌─────────────────────────────┐ │ ② extClasspath.readClass() │ 在 jre/lib/ext/*.jar 里找 └──────────┬──────────────────┘ │ 找到 ├─ 是 ──► 返回 (data, entry, nil) ✅ 结束 │ ▼ 未找到 ┌─────────────────────────────┐ │ ③ userClasspath.readClass() │ 在 -cp 指定的路径里找 └──────────┬──────────────────┘ │ ▼ 返回查找结果可能是数据也可能是 error三个要点参数不带.class后缀调用方传java/lang/Object方法内部自动补上.class。这是对外的便利设计。严格按顺序启动 → 扩展 → 用户找到即返回。这就是类路径优先级的落地。最后一步直接 return用户类路径是最后一站不管成功失败都返回它的结果失败的话 error 会一路传回调用方。String() 方法func(self*Classpath)String()string{returnself.userClasspath.String()}Classpath 的字符串表示只返回用户类路径——因为启动和扩展类路径是固定的用户关心的是自己指定的部分。七、整合到 startJVMch02 完整测试有了 Classpath改写 ch01 的startJVM()// ch02/main.gopackagemainimport(fmtstrings)importjvmgo/ch02/classpathfuncmain(){cmd:parseCmd()ifcmd.versionFlag{fmt.Println(version 0.0.1)}elseifcmd.helpFlag||cmd.class{printUsage()}else{startJVM(cmd)}}funcstartJVM(cmd*Cmd){// 1. 解析类路径cp:classpath.Parse(cmd.XjreOption,cmd.cpOption)fmt.Printf(classpath:%v class:%v args:%v\n,cp,cmd.class,cmd.args)// 2. 把类名从点分形式转成斜线形式java.lang.Object → java/lang/ObjectclassName:strings.Replace(cmd.class,.,/,-1)// 3. 读取 class 文件classData,_,err:cp.ReadClass(className)iferr!nil{fmt.Printf(Could not find or load main class %s\n,cmd.class)return}// 4. 打印字节码fmt.Printf(class data:%v\n,classData)}关键的一行className:strings.Replace(cmd.class,.,/,-1)用户在命令行输入的是java.lang.Object点分形式但 Entry 接口要求的是java/lang/Object斜线形式。这行代码做转换-1表示替换所有匹配项。编译与运行# 编译GOPATH 模式goinstalljvmgo/ch02# 或 Go Modules 模式cdch02go build-och02.exe测试 1从 rt.jar 加载 java.lang.Objectch02.exe-XjreC:\Java\jdk1.8.0_202\jrejava.lang.Object输出classpath:. class:java.lang.Object args:[] class data:[202 254 186 190 0 0 0 52 0 143 10 0 3 0 122 7 0 121 7 0 124 ... 后面跟着上千个数字看开头四个数字202 254 186 190—— 换成十六进制就是CA FE BA BE这就是 class 文件的魔数我们的 JVM 第一次从 JDK 的 rt.jar 里读出了真实的 Java 类字节码测试 2加载 JDK 的其他类ch02.exe-XjreC:\Java\jdk1.8.0_202\jrejava.lang.String ch02.exe-XjreC:\Java\jdk1.8.0_202\jrejava.util.ArrayList测试 3加载用户自己的类# 先编译一个测试类javac-dD:\test D:\test\HelloWorld.java# 用 -cp 指定ch02.exe-XjreC:\Java\jdk1.8.0_202\jre-cpD:\test HelloWorld测试 4加载不存在的类验证错误处理ch02.exe-XjreC:\Java\jdk1.8.0_202\jrecom.example.NotExist# 输出Could not find or load main class com.example.NotExist八、ch02 全景与踩坑记录完整代码结构【ch02 完整结构】 ch02/ ├── classpath/ │ ├── entry.go Entry 接口 newEntry 工厂 │ ├── entry_dir.go DirEntry目录 │ ├── entry_zip.go ZipEntryjar/zip │ ├── entry_composite.go CompositeEntry组合 │ ├── entry_wildcard.go WildcardEntry通配符 │ ├── entry_zip2.go ZipEntry 优化版缓存 │ └── classpath.go Classpath整合 ├── cmd.go 命令行解析沿用 ch01 └── main.go 程序入口 startJVM踩坑记录坑 1找不到 JRE 目录如果报错panic: Can not find jre folder!说明三级探测全失败了# 解决方案显式指定 -Xjre最稳妥ch02.exe-XjreC:\Java\jdk1.8.0_202\jrejava.lang.Object# 或者检查 JAVA_HOME 是否配置echo%JAVA_HOME%# Windowsecho$JAVA_HOME# Linux/Mac坑 2Windows 下路径含空格如果 JDK 装在C:\Program Files\Java\...路径含空格命令行要加引号ch02.exe-XjreC:\Program Files\Java\jdk1.8.0_202\jrejava.lang.Object坑 3类名格式命令行传的是java.lang.Object点分不要写成java/lang/Objectch02.exe-Xjre... java.lang.Object# ✅ 正确ch02.exe-Xjre... java/lang/Object# ❌ 错误虽然也能跑但不符合习惯坑 4Go Modules 与 GOPATH 混用如果import jvmgo/ch02/classpath报找不到包检查# Go Modules 模式下确认 go.mod 的 module 名catgo.mod# module jvmgo# import 路径必须是 模块名/子目录importjvmgo/ch02/classpath本篇小结ch02 完成我们的 JVM 具备了定位 class 文件的能力Classpath 结构体三个 Entry 字段承载启动/扩展/用户三类路径Parse()统一入口分别解析三类路径getJreDir()三级探测-Xjre选项 →./jre目录 →JAVA_HOME/jre→ panic工程味十足ReadClass()按启动 → 扩展 → 用户顺序搜索找到即返回实现类路径优先级参数便利设计对外传java.lang.Object内部自动转java/lang/Object.class首次成功从 rt.jar 读出java.lang.Object字节码开头四个字节202 254 186 190就是魔数0xCAFEBABE第二模块搜索 class 文件到此结束。我们现在能找到class 文件了但读出来的还是一堆看不懂的数字。从下一篇开始进入第三模块解析 class 文件——这是整个系列中最硬核、也最精彩的部分。我们要把那堆数字翻译成有意义的结构魔数、版本号、常量池、字段表、方法表、属性表……最终让 JVM 能看懂一个 Java 类。上一篇【第09篇】CompositeEntry 与 WildcardEntry——组合与通配符的魔法下一篇【第11篇】class 文件结构总览——JVM 的通用语