零、写在前面 JVM 的学习是每一个致力于 JAVA 语言的程序员一段最特殊的经历,至少说对于博主来说是这样的,有时候总是前脚看了,后脚就忘了。要是自己写一个 JVM,大概就很难忘了吧。带着这样的想法,博主找到一本张秀宏大神编写的《自己动手写 Java 虚拟机》,好了,话不多说,开整。
ps:博主已经把代码托管到了 GitHub 上,下面是地址
https://github.com/Mor1aty/golangJVM
一、准备工作
1、JDK jdk 安装还是必须的,具体方法这里就不赘述了,博主用的版本是 1.8。
文章图片
2、Go 在 Go 语言官网下载 Go 的安装文件,博主使用的版本是 1.5.1。需要注意的是国外的 Go 官网缘分到了才能访问,所以去 Go 语言中文网下载吧。
文章图片
下载好 Go 语言之后,在环境变量中配置 GOPATH 变量,设置一下 GO 语言的工作目录。
文章图片
最后在 cmd 中输入 go env 命令,如果显示下面类似的输出,就代表成功了。
文章图片
3、创建目录结构 先规划好项目的目录结构,下面是博主的目录结构。
文章图片
bin:存放生成的 exe 文件。
pkg:go 语言自动生成,不必理会。
resultPic:存放项目结果截图。
src:代码的主要目录结构。
文章图片
每一个章节的工程代码,以 ch + 数字的风格命名。好了前期准备就差不多了。
二、java 命令行工具分析 一开始学习 java,很多人都被告诫不要直接使用像 Eclipse、IDEA 等的 IDE 工具,多用一用命令行编译运行对初学者更好。
为什么大家都这么说呢?
因为一段 java 代码的编译运行都是离不开那些命令的。
用最经典的 Hello World 程序为例。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
如果一个类包含 main 方法,那么我们就把这个类叫做主类。一般这个类就可以用来启动 java 应用程序。java 命令有如下 4 种形式:
那么 java 虚拟机是怎么知道我们要从哪个类启动应用程序呢?这一点,java 虚拟机规范并没有明确规定。也就是说,是由虚拟机实现自行决定的。比如 Oracle 的 java 虚拟机实现是通过 java 命令来启动的,主类名由命令行参数指定。
java [-options] class [args]
java [-options] -jar jarfile [args]
javaw [-options] class [args]
javaw [-options] -jar jarfile [args]
java 命令的具体含义这里就不赘述了,感兴趣的朋友可以去看张大神的书。
三、命令行工具代码构建 好了,经过上面简单的分析,我们大致已经明白要做什么样的东西了。那就开始吧。
下面的代码是根据 java 命令的第一种形式,编写的一个类似的命令行工具。
先在 ch01 目录下创建 cmd.go 文件,用你喜欢的文本编辑器打开,博主使用的是 VSCode,然后在其中定义一个 Cmd 结构体,用来表示命令行选项和参数。下面是代码:
package mainimport "flag"
import "fmt"
import "os"type Cmd struct{
helpFlag bool // 参数 help 的标记
versionFlag bool // 参数 version 的标记
cpOption string // 参数 cp 的标记
class string // class 类
args []string // 参数 string 数组
}
在 Go 语言中和 java 一样内置了许多功能强大的包。我们将用到 fmt,os 和 flag 包。
在 os 包中定义了一个 Args 变量,在其中存放的是传递给命令行的全部参数。我们可以编写相应的处理代码来处理这些参数。不过,Go 语言为我们内置了 flag 的包,这个包可以帮我们处理命令行选项。
继续编写 cmd.go 文件,代码如下:
// 定义一个 parseCmd() 函数,使用 flag 帮我们解析
func parseCmd() *Cmd{
cmd := &Cmd{}
// 先将 flag.Usage 赋值一个 printUsage 的函数。这个函数由我们手动编写。
// printUsage() 函数主要是在我们解析选项失败之后返回一些信息
flag.Usage = printUsage
// 使用 flag 提供的各种 Var() 函数设置需要解析的选项
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.Parse()
// 使用 flag 的 Args() 函数捕获其他的参数
args := flag.Args()
if len(args) > 0 {
// 第一个参数作为类,剩下的参数存到 args 中
cmd.class = args[0]
cmd.args = args[1:]
}
return cmd
}
接下来编写 printUsage() 函数:
func printUsage(){
fmt.Printf("Usage: %s [-option] class [args...]\n",os.Args[0])
}
用了简单的几十行代码,我们的命令行工具就编写好了。
四、测试代码构建 在 ch01 目录下创建 main.go 文件,然后输入下面代码:
package mainimport "fmt"func main(){
// 调用 parseCmd() 函数解析命令行参数
cmd := parseCmd()
if cmd.versionFlag{
// 是 version
fmt.Println("version 0.0.1")
}else if cmd.helpFlag || cmd.class == "" {
// 是 help/?/""
printUsage()
}else{
// 其他的就调用 startJVM()
startJVM(cmd)
}
}
注意,cmd.go 和 main.go 的包名都是 main,因为在 Go 语言中,main 是一个特殊的包,这个包所在的目录会被编译成可执行文件。Go 语言中的代码入口同样是 main() 函数,但不能有任何参数,也不能有返回值。
main() 函数先调用 parseCmd() 函数,得到 cmd 变量,如果用户输入 -version,则输出 “version 0.0.1”,如果输入的是 -help,或者解析出现错误,就调用 printUsage() 函数输出帮助信息,其他情况就调用 startJVM() 函数,好,接下来就编写 startJVM() 函数。
func startJVM(cmd *Cmd){
fmt.Printf("classpath: %s class:%s args:%v\n",cmd.cpOption,cmd.class,cmd.args)
}
这样我们的测试代码也就结束了,那开始编译执行吧。
五、编译运行 打开 cmd 窗口,cd 到工程的 src 目录下,执行下面的命令。
go install jvmgo\ch01
文章图片
出现这样的情况就代表成功了。这个时候就会在与 src 同级的 bin 目录下生成 ch01.exe 文件。
好了,开始测试吧。
文章图片
六、小结 【Go语言实现 JVM(一)命令行工具】这里尽管我们还没有正式开始编写 java 虚拟机,但是打好了一个不错的基础,编写了一个简化版的命令行工具,是一个不错的开始。