【DIY系列:Java虚拟机】第05篇:项目结构与第一行代码——jvmgo 项目骨架搭建

发布时间:2026/9/7 13:31:59

【DIY系列:Java虚拟机】第05篇:项目结构与第一行代码——jvmgo 项目骨架搭建
上一篇【第04篇】Go 语言环境准备——用 Go 写 JVM 的 5 大理由下一篇【第06篇】类路径classpath是什么鬼——JVM 怎么找到你的 class 文件摘要前面四篇都在做准备这一篇终于要动手写代码了本章ch01的目标很朴素写一个类似java命令的命令行工具能解析-classpath、-version、-help这些参数并把解析结果打印出来。虽然它还不能执行任何字节码但这是整个 jvmgo 项目的起点。本文会带你规划 jvmgo 的 11 章目录结构、创建项目骨架、设计 Cmd 结构体、用 Go 的 flag 包解析命令行参数、实现版本号与帮助信息输出最后完整测试 ch01。写完这篇你就拥有了一个还啥也干不了的Java 虚拟机启动器。一、jvmgo 的整体项目规划在动手之前先把整个项目的版图规划清楚。jvmgo 采用渐进式演进的组织方式整个项目分成ch01 到 ch11 共 11 个版本每个版本是一个独立的 Go 程序后一章在前一章的基础上增量开发。【jvmgo 目录结构演进】 jvmgo/ ├── ch01/ 第1章命令行工具 │ ├── cmd.go 命令行参数解析 │ └── main.go 程序入口 │ ├── ch02/ 第2章搜索 class 文件 │ ├── classpath/ │ │ ├── entry.go Entry 接口 │ │ ├── entry_dir.go DirEntry │ │ ├── entry_zip.go ZipEntry │ │ ├── entry_composite.go CompositeEntry │ │ ├── entry_wildcard.go WildcardEntry │ │ └── classpath.go Classpath │ ├── cmd.go │ └── main.go │ ├── ch03/ 第3章解析 class 文件 │ ├── classfile/ ★新增class 文件解析 │ │ ├── class_reader.go 字节读取器 │ │ ├── class_file.go class 文件结构 │ │ ├── constant_pool.go 常量池 │ │ ├── constant_info.go 14 种常量 │ │ ├── attribute_info.go 属性表 │ │ └── member_info.go 字段/方法表 │ ├── classpath/ │ ├── cmd.go │ └── main.go │ ├── ch04/ 第4章运行时数据区 │ ├── rtda/ ★新增运行时数据区 │ │ ├── thread.go Thread │ │ ├── stack.go Stack │ │ ├── frame.go Frame 栈帧 │ │ ├── local_vars.go 局部变量表 │ │ ├── operand_stack.go 操作数栈 │ │ └── object.go Object临时 │ ├── classfile/ │ ├── classpath/ │ └── main.go │ ├── ch05/ 第5章指令集和解释器 │ ├── instructions/ ★新增字节码指令150 │ │ ├── instruction.go Instruction 接口 │ │ ├── base/ │ │ ├── constants/ 常量指令 │ │ ├── loads/ 加载指令 │ │ ├── stores/ 存储指令 │ │ ├── stack/ 栈指令 │ │ ├── math/ 数学指令 │ │ ├── conversions/ 转换指令 │ │ ├── comparisons/ 比较指令 │ │ ├── control/ 控制指令 │ │ ├── references/ 引用指令 │ │ └── extended/ 扩展指令 │ ├── rtda/ │ ├── interpreter.go ★新增解释器 │ └── main.go │ ├── ch06/ ~ ch11/ 后续章节持续增量 │ ├── rtda/heap/ ★ch06 新增方法区、类加载器 │ └── native/ ★ch09 新增本地方法每一章的目录变化章节新增目录/文件累计代码量约ch01cmd.go, main.go~60 行ch02classpath/~300 行ch03classfile/~1200 行ch04rtda/~1700 行ch05instructions/, interpreter.go~4700 行ch06rtda/heap/~6500 行ch07增量修改~7500 行ch08增量修改~8500 行ch09native/~10000 行ch10增量修改~10500 行ch11增量修改~11000 行重点每一章都在上一章基础上复制一份再改。比如从 ch02 到 ch03你只需要cp -r ch02 ch03然后把 main.go 里的 import 路径ch02改成ch03再新增 classfile 目录。这种快照式组织方式的好处是每一章的代码都是完整可运行的随时可以回头对比。二、创建项目骨架开工。先创建项目根目录和 ch01# 创建项目目录mkdirjvmgocdjvmgo# 初始化 Go 模块go mod init jvmgo# 创建 ch01 目录mkdirch01cdch01创建两个文件cmd.go和main.go。【ch01 的文件结构】 jvmgo/ ├── go.mod ← go mod init 生成 └── ch01/ ├── cmd.go ← 命令行参数解析 └── main.go ← 程序入口main 函数go.mod内容module jvmgo go 1.21三、先看看真正的 java 命令长什么样在写代码之前我们先研究一下要模仿的对象——JDK 的java命令。在命令行里敲java-help会看到一大堆参数。我们不需要实现全部只实现最核心的几个【java 命令的核心参数】 java [-options] class [args...] │ │ │ │ │ └─ 传给 main 方法的参数 │ │ (String[] args) │ │ │ └─ 要运行的主类全限定名 │ 例如com.example.HelloWorld │ └─ JVM 选项 ├─ -version 打印版本信息并退出 ├─ -help / -? 打印帮助信息并退出 ├─ -classpath / -cp 指定类路径 │ 例如-cp .;lib/* └─ -Xjre ★我们自己加的指定 JRE 路径参数解析的优先级是这样的【java 命令参数解析流程】 启动 java │ ▼ 解析命令行参数 │ ├─ 有 -version ── 是 ──► 打印版本号 ──► 退出 │ ├─ 有 -help / - ── 是 ──► 打印帮助 ──► 退出 │ ├─ 主类为空 ── 是 ──► 打印帮助 ──► 退出 │ └─ 都不满足 ──► 启动 JVM加载主类调用 main()重点-Xjre这个参数在原版 java 命令里是没有的至少不是标准用法是我们为了指定 JRE 路径以便加载 rt.jar自己加的。JVM 里以-X开头的通常是非标准选项所以我们遵循这个约定。四、Cmd 结构体命令行参数的容器先定义存放解析结果的结构体。ch01/cmd.gopackagemainimportflagimportfmtimportos// Cmd 结构体用于保存解析后的命令行参数typeCmdstruct{helpFlagbool// -help / -? 是否指定versionFlagbool// -version 是否指定cpOptionstring// -classpath / -cp 的值XjreOptionstring// -Xjre 的值classstring// 主类名args[]string// 传给 main 方法的参数}六个字段对应前面分析的全部参数。重点注意XjreOption这个字段名首字母大写——在 Go 里首字母大写才能被其他包访问。虽然 ch01 里所有代码都在main包内大写小写都行但为了后续章节方便Cmd 会被其他包使用这里保持大写。parseCmd()解析命令行参数Go 标准库的flag包专门干这事用起来很简洁// parseCmd 解析命令行参数返回 Cmd 指针funcparseCmd()*Cmd{cmd:Cmd{}// 1. 设置自定义的 usage 函数flag.UsageprintUsage// 2. 注册需要解析的参数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,classpath,,classpath)flag.StringVar(cmd.cpOption,cp,,classpath)flag.StringVar(cmd.XjreOption,Xjre,,path to jre)// 3. 执行解析flag.Parse()// 4. 解析剩下的非选项参数args:flag.Args()iflen(args)0{cmd.classargs[0]// 第一个是主类名cmd.argsargs[1:]// 剩下的是 main 方法的参数}returncmd}flag包的核心 API函数作用例子flag.BoolVar(v, name, default, usage)注册布尔型选项flag.BoolVar(help, help, false, 帮助)flag.StringVar(v, name, default, usage)注册字符串选项flag.StringVar(cp, cp, , 类路径)flag.IntVar(v, name, default, usage)注册整型选项—flag.Parse()执行解析必须调用flag.Args()获取非选项参数[HelloWorld, arg1]flag.Usage自定义 usage 输出覆盖默认帮助注意几个细节同一个变量可以注册多个名字-help和-?都绑定到cmd.helpFlag-classpath和-cp都绑定到cmd.cpOption。这跟真实的 java 命令一致。flag.Parse()之后flag.Args()返回所有非选项参数即不以-开头的那些。flag.Usage是个函数变量赋值后当解析出错或用户请求帮助时会自动调用它。printUsage()帮助信息// printUsage 打印帮助信息funcprintUsage(){fmt.Printf(Usage: %s [-options] class [args...]\n,os.Args[0])}os.Args[0]是程序自己的路径所以帮助信息会显示成类似Usage: D:\jvmgo\ch01\ch01.exe [-options] class [args...]。五、main.go程序入口有了参数解析主程序就简单了。ch01/main.gopackagemainimportfmtfuncmain(){cmd:parseCmd()ifcmd.versionFlag{// 用户要版本号fmt.Println(version 0.0.1)}elseifcmd.helpFlag||cmd.class{// 用户要帮助或者压根没给主类名printUsage()}else{// 正常启动 JVMstartJVM(cmd)}}// startJVM 启动 JVM本章只是打印参数后面章节逐步实现funcstartJVM(cmd*Cmd){fmt.Printf(classpath:%s class:%s args:%v\n,cmd.cpOption,cmd.class,cmd.args)}逻辑很直白就是前面那张流程图的直接翻译有-version→ 打印版本号有-help或者没给主类名 → 打印帮助否则 → 启动 JVM本章只打印参数重点startJVM()现在只是个占位函数打印一下参数就完事。在接下来的 10 章里这个函数会不断长大——ch02 用它来测试类路径ch03 用它打印 class 文件内容ch05 开始真正执行字节码……到 ch11它就是一个完整的 JVM 启动器了。六、编译与测试代码写完了跑起来看看。编译cdch01 go build-ojvmgo.exe生成一个jvmgo.exeWindows或jvmgoMac/Linux可执行文件。测试 1打印版本号$ jvmgo.exe-versionversion0.0.1测试 2打印帮助$ jvmgo.exe-helpUsage: jvmgo.exe[-options]class[args...]$ jvmgo.exe Usage: jvmgo.exe[-options]class[args...]注意第二个测试不带任何参数时也打印帮助因为cmd.class 。测试 3正常启动$ jvmgo.exe-cpD:\testHelloWorld arg1 arg2 classpath:D:\test class:HelloWorld args:[arg1 arg2]参数解析成功-cp的值、HelloWorld主类名、以及arg1 arg2两个额外参数都被正确解析了。测试 4等价写法# -classpath 和 -cp 是等价的$ jvmgo.exe-classpathD:\testHelloWorld classpath:D:\test class:HelloWorld args:[]# -Xjre 也能解析$ jvmgo.exe-XjreC:\Java\jdk1.8.0_202\jre-cpD:\testHelloWorld classpath:D:\test class:HelloWorld args:[]七、踩坑记录坑 1Windows 下路径分隔符Windows 的类路径用分号;分隔多个路径Linux/Mac 用冒号:# Windowsjvmgo-cp.;lib/*;C:\Java\rt.jarHelloWorld# Linux / Mac./jvmgo-cp.:lib/*:/usr/lib/jvm/rt.jarHelloWorld后面实现 CompositeEntry 时要处理这个差异——用os.PathListSeparator获取当前系统的分隔符而不是硬编码。坑 2命令行参数的解析顺序flag包要求选项必须在非选项参数之前否则后面的选项会被当成普通参数# ✅ 正确选项在前jvmgo-cpD:\testHelloWorld arg1# ❌ 错误HelloWorld 后面的 -cp 不会被解析成选项jvmgo HelloWorld-cpD:\testarg1# 结果classHelloWorld, args[-cp, D:\test, arg1]这跟真实的 java 命令行为是一致的java HelloWorld -cp xxx里的-cp确实会被当作 main 方法的参数。坑 3Windows 命令行中文乱码如果你的 Go 程序输出中文乱码在命令行先执行chcp65001# 切换到 UTF-8 代码页本篇小结ch01 完成了 jvmgo 项目的第一步——一个 java 命令的雏形项目规划明确 ch01~ch11 共 11 个版本的渐进式演进结构每章复制上一章再增量开发Cmd 结构体6 个字段承载全部命令行参数helpFlag/versionFlag/cpOption/XjreOption/class/args参数解析用 Go 标准库flag包BoolVar/StringVar注册选项Parse()执行解析Args()获取主类名和额外参数主程序逻辑-version→ 打印版本-help或无主类名 → 打印帮助否则 → startJVM()完整测试4 组测试全部通过参数解析正确现在我们的startJVM()还只是个打印参数的空壳。但别急——下一章ch02就要给它加上**类路径classpath**功能让我们的 JVM 能够找到 class 文件在磁盘上的位置。从下一章开始就是真正的 JVM 核心内容了。上一篇【第04篇】Go 语言环境准备——用 Go 写 JVM 的 5 大理由下一篇【第06篇】类路径classpath是什么鬼——JVM 怎么找到你的 class 文件

相关新闻

【DIY系列:Java虚拟机】第06篇:类路径(classpath)是什么鬼——JVM 怎么找到你的 class 文件

【DIY系列:Java虚拟机】第06篇:类路径(classpath)是什么鬼——JVM 怎么找到你的 class 文件

2026/9/7 13:31:59

上一篇【第05篇】项目结构与第一行代码——jvmgo 项目骨架搭建 下一篇【第07篇】Entry 接口设计——类路径的"积木块" 摘要 写一个 JVM,第一件要解决的事就是:去哪儿找 class 文件? 有意思的是,Java 虚拟机规范并没有…

计算机系统结构实验指南:从模拟器配置到报告撰写的完整流程

计算机系统结构实验指南:从模拟器配置到报告撰写的完整流程

2026/9/7 13:31:59

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

770B MoE开源模型Hy4 preview解析:架构、部署与WorkBuddy实操

770B MoE开源模型Hy4 preview解析:架构、部署与WorkBuddy实操

2026/9/7 13:31:59

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

FLUX 3 AI图像生成工具:复古界面下的现代内核与实用指南

FLUX 3 AI图像生成工具:复古界面下的现代内核与实用指南

2026/9/7 14:52:03

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

软考中级系统集成项目管理工程师300集精讲:零基础备考全攻略

软考中级系统集成项目管理工程师300集精讲:零基础备考全攻略

2026/9/7 14:52:03

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

GeoLibre轻量化WebGIS实战:开源部署与前端集成指南

GeoLibre轻量化WebGIS实战:开源部署与前端集成指南

2026/9/7 14:52:03

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

DeerFlow Blocking-IO Guard 实战:用运行时锚点与牙齿验证守住 asyncio 事件循环

DeerFlow Blocking-IO Guard 实战:用运行时锚点与牙齿验证守住 asyncio 事件循环

2026/9/7 14:52:03

DeerFlow Blocking-IO Guard 实战:用运行时锚点与牙齿验证守住 asyncio 事件循环 【免费下载链接】deer-flow An open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagen…

WeChatMsg微信聊天记录导出实操:10分钟把对话变成可检索档案

WeChatMsg微信聊天记录导出实操:10分钟把对话变成可检索档案

2026/9/7 14:52:03

WeChatMsg微信聊天记录导出实操:10分钟把对话变成可检索档案 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we…

Linux服务器运维指令全攻略:从SSH、tmux到conda的高频命令速查

Linux服务器运维指令全攻略:从SSH、tmux到conda的高频命令速查

2026/9/7 14:42:02

2026年元旦刚过完,我在整理手头几个服务器的部署文档时发现一个尴尬局面:同一个排查指令,这半年里我至少搜过五六次,每次都要重新翻博客、翻手册,偶尔还翻到过时写法。索性决定从1月5日开始,把自己在服务器…

中国人民大学杨琳团队《Nature Communications》 | 全球潮汐湿地土壤有机碳时空格局与环境驱动:一项2009-2020年的全球评估

中国人民大学杨琳团队《Nature Communications》 | 全球潮汐湿地土壤有机碳时空格局与环境驱动:一项2009-2020年的全球评估

2026/9/6 1:19:56

本文首发于“生态学者”!从“湿地面积”到“土壤碳密度”:为什么需要重新认识潮汐湿地蓝碳变化?潮汐湿地位于陆地与海洋的交汇地带,包括红树林、盐沼和潮滩,是全球重要的蓝碳生态系统。其土壤能够长期储存大量有机碳&a…

adb抓包

adb抓包

2026/9/7 3:44:24

前言 本文介绍如何通过 tcpdump 在 Android 手机上抓取网络数据包,并在电脑端使用 Wireshark 进行分析。适用于需要排查 App 网络请求、分析接口调用或调试网络问题的开发与测试场景。1. 手机要有 root 权限2. 下载 tcpdump3. adb push C:\Users\zhangkuixun\Downlo…

大模型推理镜像极简瘦身:从 25GB 巨无霸到 3GB 精简镜像实战

大模型推理镜像极简瘦身:从 25GB 巨无霸到 3GB 精简镜像实战

2026/9/7 8:03:37

大模型推理镜像极简瘦身:从 25GB 巨无霸到 3GB 精简镜像实战 在云原生基础设施中,容器镜像体积直接决定了服务的部署速度与弹性扩容敏捷度。对于传统的 Go / Java 微服务,镜像体积通常被严格控制在 50MB 到 200MB 以内,拉取镜像只…

基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现

基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现

2026/9/7 0:01:24

这次我们来看一个把目标检测算法和桌面端工具结合得很典型的项目:基于 YOLOv8 PyQt5 的麦穗稻穗检测识别系统。这个项目本身不是新概念,但它的价值在于落地形态很完整。YOLOv8 负责核心的麦穗稻穗目标检测,PyQt5 负责提供可视化的桌面交互界…

UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南

UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南

2026/9/7 0:01:24

简介:UL 1642是锂电池安全领域的重要规范,本中文版资源适合锂电池制造商、检测机构工程师及产品认证相关人员阅读,用于理解电池在设计与制造层面的安全要求、测试方法与合规要点。资源共1个PDF文件,压缩包大小834KB,便…

BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析

BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析

2026/9/7 0:01:24

简介:BS EN 13814-1:2019是英国采纳欧洲标准EN 13814-1:2019的正式版本,由BSI标准出版,重点规定游乐设施和游乐设备在设计与制造环节的安全准则,与BS EN 13814-2:2019、BS EN 13814-3:2019共同取代旧版BS EN 13814:2004。该标准面…

远程协作的工作台整理

远程协作的工作台整理

2026/9/7 3:38:07

远程协作的工作台整理远程协作的核心不是再加一个工具,而是让交接信息足够完整。异步任务要写明目标、输入位置、完成标准和需要决策的人。 工作台的最小配置 将日程、待办、代码和沟通入口收拢到少数固定位置;通知按紧急程度分层。工作台不需要模仿办公…

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

2026/9/4 7:42:10

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

2026/9/6 23:21:51

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…