本系列文章主要根据张秀宏老师的—— 《自己动手写 java 虚拟机》一书所做的笔记。该书实现了大部分 JVM 的功能,包括class文件解析、类加载、指令集、解释器、方法调用、数组和字符串的处理、异常处理等。这本书可以说是了解 Java 虚拟机最浅显易懂的书籍,随书提供了每个章节的代码,可以完美运行。但是这本书所附带的代码是用 go 语言书写的,如果想要看懂这本书的代码,首先要学会 go 语言的基本语法。这边博客记录了在阅读这本书之前的准备工作。我在练习中是根据张老师的代码用 Java 语言又实现了一遍。虽然用 Java 语言在实现一个自己的虚拟机,然后跑在自带的虚拟机上有点滑稽,但是重点还是了解虚拟机内部的基本原理。

贴出张秀宏老师这本书的代码

如果你想跟着张秀宏老师的代码学习 JVM,那么下面的内容正是为你准备的。而如果你不想花时间学习另一门语言,那么可以参考我用 Java 实现的相同功能的代码,下面关于 go 的准备工作,可以忽略。

安装 Go 开发环境

下载

Go 语言中文网,这个下载速度快一点,下载最新版本之后一路next即可。

安装后,其实安装脚本已经自动帮我们把 Go 的安装路径添加到系统的环境变量中了, 查看系统环境变量,发现多了一个 GOROOT,其值为:C:\go(默认的安装路径)并且在path中,也自动把 GOROOT\bin添加了进来,因此直接打开命令行窗口,输入go version命令,就会输出安装的go的版本信息.

配置环境

既然上一步安装Go时已经添加了环境变量了,那么接下来还要配置什么呢?

Go希望我们把所有的代码都存在在同一个目录下,这就好比我们用eclipse时需要一个workspace,但是eclipse中的workspace可以有多个,而Go希望在一台主机上只有一个workspace,所以我们在这里要指定一个,例如D:\workspace,这个位置随意,文件夹名字随意,然后和Go进行关联,这里的关联方法就是在环境变量中,添加一个GOPATH,其值为刚刚设定的D:\workspace,其指明了除GOROOT之外的包含 Go 项目源代码和二进制文件的目录。这和Java中的 classpath 有些类似。当然你可以为GOPATH指定多个路径作为 workspace 的,但是这里还是建议一个就好。

接下来,在D:\workspace中添加一个目录src,这一步是必须的,所有的Go源代码都必须放在这个src目录下,那有人问,如果我有多个项目,所有源代码都放在这个目录下,怎么分清哪个源代码归属于哪个项目呢? 方法很简单,就是在src目录下再用不同的文件夹作为分隔,一个项目一个文件夹,自然就分开了;

Go 语言的包结构

Go语言以包为单位组织源代码,包可以嵌套,形成层次关系。书中编写的Go源文件全部放在jvmgo包中,其中每一章的源文件又分别放在自己的子包中。包层次目录结构有一个简单的对应关系,比如,第 1 章的代码在jvmgo\ch01目录下。除第 1 章以外,每一章都是先复制前一章代码,然后进行修改和完善。每一章的代码都是独立的,可以单独编译为一个可执行文件。

所形成的目录结构是:

1
2
3
4
5
6
- D:\workspace
- src
- jvmgo
- ch01
- ch02
...

之前也说过在 src 下,通过文件夹将不同的项目分割开。这里的例子是使用了个二级文件夹来区分的,每个项目都是放在src/jvmgo/chXXX下的,我们平时用一级目录当然也是可以的。那么其目录看起来应该是这样的:

1
2
3
4
5
- D:\workspace
- src
- ch01
- ch02
...

在每一个章节的 chX(go 的工程)中,都必须有一个main的包,这个包中的某个文件必须要包含一个main方法,因为这里每个文件夹被看做一个独立的工程。这个也很好理解,因为Java工程中,每个Java都可以有一个public static void main(String[] args)方法,这一点不足为奇,但是要注意的是,一旦你选定某个文件夹为一个项目(eg:jvmgo/ch05),并且使用 go install jvmgo\ch05

来生成一个 exe 文件, 那么这个目录下只能有一个main方法,不能有多个,否则就报错,类似于

1
2
D:\workspace\src\jvmgo\ch05\main2.go:3: main redeclared in this block
previous declaration at E:\workspace\src\jvmgo\ch05\main.go:8

这是因为我在main.go中声明了一个main方法,又在main2.go中声明了一个main方法,所以就报了重复定义的错误。

还有一点要说明的是:可以直接在命令行输入 go install your/dir/name系统会自动从GOROOT/srcGOPATH/src两个文件中找your/dir/name,而不用非进入到GOPATH/src路径下执行,而且如果your/dir/name在上述两个路径中均存在,那么前者优先级更高。

前面是使用go install命令生成可执行文件,但是如果你使用 go run xxx.go,那么不用保证 xxx.go 所在的包中main方法是唯一的,main包中其它文件也可以包含main方法,因为你运行的是单个文件,不过这只是自己平时练习时用,真正的项目都不会只有单个文件那么简单。

关于 Go 语言的包结构,下面两篇文章是不错的参考。

GO 项目的目录结构

GOROOT、GOPATH 和 project 目录说明

快速入门 Go 语言

因为本项目是想用Java实现书中的JVM项目,而书中所提供的是Go的源码,所以这里只要求能看懂Go的代码就可以,不必深入细节,相信看到这篇文章,想学习JVM,那么你的Java语言一定已经很熟练了,那么再快速学习一门语言是很快的,这里推荐Go 的官方教程,一天就能看完,一定要有耐心。

上面提供的网站很不稳定,如果人品好的话,打开之后,赶紧一口气学完;运气差点肯定就打不开,没关系,这里有离线的版本,效果是一样的;

如果你已经安装好了Go的开发环境,那么直接打开命令行,输入go tool tour,浏览器就会自动打开一个页面,和在线版是一样,而且速度很快,你可以在右侧的代码区编写自己的代码并运行,这里代码的改动都是保存在本地的。

go 常用命令

  • go build:编译出可执行文件,默认位置是和 main 方法所在的文件同目录下。
  • go run
  • go install:先执行go build命令,然后把最终编译成的可执行文件放到 GOPATH/bin 目录下。
  • go get:从指定源上面下载或者更新指定的代码和依赖(git clone),并对他们进行编译和安装(go install)。

依然用上面的例子来说明,例如我们下得到第 1 章(每一章都是一个单独的项目,相互之前没有关系)的一个可执行程序。

go build

构建编译由导入路径命名的包,以及它们的依赖关系,但它不会安装结果

使用方法:

1
go build [-o 输出名] [-i] [编译标记] [包名]

其具体参数可以参考下面给出的参考链接,这里简单提一下 go build 的使用:

  1. 当编译单个 main 包(文件),则生成可执行文件。
  2. 当编译单个或多个包非主包时(不包含 main 方法),只构建编译包,但丢弃生成的对象(.a)仅用作检查包可以构建
  3. 当编译包时,会自动忽略’_test.go’的测试文件。

在命令行进入到GOPATH/src/jvmgo/ch01路径,执行:go build main.go,这样就会产生一个 main.exe 的可执行文件(默认情况下这个文件的名字为源文件名字去掉.go 后缀),其与 main.go 在同一目录下。但是如果你真的这样做是无法生成 main.exe 可执行文件的,因为 build 命令构建单个文件时,虽然满足上面的条件 1,但是该文件中还使用了其它的文件中的方法。我们平时自己写一个简单的打印 helloword 的程序,所有代码都在一个文件中,用该命令是可以得到一个可执行文件的,但是如果我们的文件中依赖了其它文件中的方法,那么这时候就会报错,找不到 xxx 方法。所以这个命令平时使用的不是很多,用的最多的还是 install 命令。

参考:go build

go run

和 go build 类似,用来运行单个包含 main 方法的文件,其不生成 exe 文件,而是直接在命令行中运行该 main 文件。该命令常用来测试一些功能。

go install

在命令行任意路径下执行:go install jvmgo/ch01,这样就会在GOPATH/bin路径下产生一个 ch01.exe(install 命令以 main 方法所在的上一级目录名作为可执行程序名)的可执行文件。之前我们也提到了,必须在 GOPATH 路径下手动创建一个 src 的文件夹,并将我们写的所有的 go 文件都刚在该路径下,但执行了上面的 install 命令后,发现在 GOPATH 下除了 src,多出了两个文件夹。下面解释下这三个文件夹的作用。

  • src 包含项目的源代码文件;
  • pkg 包含编译后生成的包/库文件;
  • bin 包含编译后生成的可执行文件。

这里特别要提一下的是 pkg 文件夹,其所放的内容是声明?我们知道大型项目中都不可能将所有代码都写在一个文件中的,而是要分包。那么各个包在 go install 时,会被编译成 xxx.a,类似 C 中的链接库。

但是 go install 也是有坑的,详见:go install 的工作方式

go 中的文件夹名和包名

在 Java 中,类的包名和文件路径必须是对应的,而在 go 中则没有这种硬性规定,但是为了规范代码,Google 官方还是强烈建议我们写 go 代码时,文件夹名和包名保持一致。如果不一致,例如我们有一个文件夹,名为 A,其下有个 go 文件,自定义的包名是 B(每个子目录中只能存在一个 package,否则编译时会报错),那么在其它文件中使用该文件中的方法时,需要import A,而在代码中使用其方法时,则使用B.func()。所以这还是强烈建议,包名和所在文件夹名保持一致

这里有一个特殊的包,叫做main,在执行 go install 命令时,如何找到整个程序的入口呢?光有 main 方法是不行的,包含 main 方法的 go 文件,其包名必须也是main,同时所有包名为main的 go 文件中,只能有一个 main 方法。只有满足以上所有要求,才会得到可执行文件,如果 main 方法不在main下,执行 go install ,go 会将所有包都视为 library,在 pkg 下生成对应的 xxx.a 文件,而不会有可执行文件。

关于开发工具

这里推荐使用 Intellij idea + 插件的方式,这里主要是为了看代码方便,可以很方便的实现方法之间的跳转,

go 插件安装

打开ideasetting->plugin,搜索go,点击下方的Browse repositorise,找到Go language(golang.org)support plugin,官网地址,安装后重启编译器插件才可以用。

其实用 idea+插件的方式,查看 go 的代码已经很舒服了,不用额外再配置其他 ide,毕竟我们的主要精力还是能看懂 go 的代码,并不写任何 go 代码。

注意:安装 go 插件,需要 idea 的版本为2016.2,我在开发中使用的 idea 为 COMMUNITY 2016.2.5,最新版本的 idea 无法安装 go 插件!

bytecode 插件安装

既然虚拟机是和 class 文件打交道的,就免不了查看字节码的内容,这里提供一个好用的查看字节码的插件——jclasslib Bytecode viewer,安装方式同 go 插件的安装。

使用方法:首先要单独编译当前窗口的 java 文件,使其生成 class 文件,然后在编译器的View中选择show Bytecode with jclasslib

用 idea 看源码

为了配合idea阅读《自己动手写 java 虚拟机》的源码,这里最简单的做法:

  1. 新建一个 go 项目,命名为jvmgo,路径为GOPATH/src
  2. 需要看哪一章的源码,就把作者对应的 jvmgo/文件夹下的ch01(以第一章为例),复制到上一步新建的项目路径下,最终结果应该是:GOPATH/src/jvmgo/ch01