bboyjing's blog

自己动手写JVM二【搜索class文件】

1
2
3
4
5
public class HelloWorld {
public static void main(String[] args){
System.out.println("Hello, world!");
}
}

上面是一段最简单可运行Java代码了,要运行这么一段简单的代码,其实要做很多工作。加载HelloWorld类之前,首先要加载它的超类java.lang.Object,在调用main()函数之前,虚拟机要准备好参数数组,所以需要加载java.lang.String和java.lang.String[]类。把字符串打印到控制台还需要加载java.lang.System类,等等。。。这一章节就来学习下Java虚拟机是从哪里寻找这些类的。

类路径

类路径可以分为如下三个部分:

  • 启动类路径(bootstrap classpath)
  • 扩展类路径(extention classpath)
  • 用户类路径(user classpath)

启动类路径默认对应jre/lib目录,Java标准库位于该路径。扩展类路径默认对应jre/lib/ext目录,使用Java扩展机制的类位于该路径。我们自己实现的类,以及第三方类库则位于用户类路径。用户类路径的默认值是当前路径,也就是”.”,可以给java命令传递-classpath选项来指定。
本章示例代码位于classpath包下,同时会修改和完善第一章的cmd.go。

1
2
3
//在对应位置添加XjreOption
XjreOption string
flag.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre")

实现类路径

采用组合模式来实现类路径,把类路径当成一个大的整体,由启动类路径、扩展类路径和用户类路径三个小路径构成,三个小路径又分别由更小的路径构成。
下面是Entry接口的部分代码,如果主要只写过Java,可能会不太容易理解Go的接口实现。Entry一共有四种实现,下面挑一个DirEntry来详细看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package classpath
import "os"
/*
获取系统分隔符
Windows下是;类UNIX系统下是:
*/
const pathListSeparator = string(os.PathListSeparator)
// 定义Entry接口
type Entry interface {
// 负责寻找和加载class文件
readClass(className string) ([]byte, Entry, error)
// 返回变量的字符串表示,相当于Java中的toString
String() string
}

DirEntry表示目录形式的类路径,Go和Java语言不同,Go不需要显示实现接口,只要方法匹配即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package classpath
import (
"path/filepath"
"io/ioutil"
)
// 定义DirEntry结构体
type DirEntry struct {
// 用于存放绝对路径
absDir string
}
/*
返回指向DirEntry对象的指针
Go语言没有专门的构造函数,此函数就当做DirEntry的构造函数
*/
func newDirEntry(path string) *DirEntry {
/*
Go使用error值来表示错误状态
Go使用panic和recover来处理所谓的运行时异常
*/
absDir, err := filepath.Abs(path)
if(err != nil) {
panic(err)
}
return &DirEntry{absDir}
}
/*
Go没有类,可以使用方法接受者的方式在结构体类型上定义方法
指向DirEntry对象的指针self为方法接受者
该方法用来读取class文件文件
*/
func (self *DirEntry) readClass(className string) ([]byte, Entry, error) {
fileName := filepath.Join(self.absDir, className)
data, err := ioutil.ReadFile(fileName)
return data, self, err
}
// 实现String方法
func (self *DirEntry) String() string {
return self.absDir
}

其他三个实现,这里就不贴出来了,项目中有。四种类路径都实现完之后,再来完善下Entry接口,添加Entry实例的构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func newEntry(path string) Entry {
//如果path中包含分隔符,则实例化CompositeEntry
if (strings.Contains(path, pathListSeparator)) {
return newCompositeEntry(path)
}
//如果path以*结尾,则实例化WildcardEntry
if (strings.HasSuffix(path, "*")) {
return newWildcardEntry(path)
}
//如果path以jar或者zip结尾,则返回ZipEntry
if strings.HasSuffix(path, ".jar") ||
strings.HasSuffix(path, ".JAR") ||
strings.HasSuffix(path, ".zip") ||
strings.HasSuffix(path, ".ZIP") {
return newZipEntry(path)
}
//以上都不匹配则返回DirEntry
return newDirEntry(path)
}

实现Classpath结构体

Classpath结构体有三个字段,分别存放三种类路径,classpath.go代码有点长,这里截取核心的部分展示出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Classpath结构体
type Classpath struct {
bootClasspath Entry
extClasspath Entry
userClasspath Entry
}
/*
Parse函数使用-Xjre选项解析启动类路径和扩展类路径
使用-classpath/-cp选项解析用户类路径
*/
func Parse(jreOption, cpOption string) *Classpath {
cp := &Classpath{}
// 解析启动类路径和扩展类路径
cp.parseBootAndExtClasspath(jreOption)
// 解析用户类路径
cp.parseUserClasspath(cpOption)
return cp
}
// 解析启动类路径和扩展类路径方法
func (self *Classpath) parseBootAndExtClasspath(jreOption string) {
// 获取jre目录
jreDir := getJreDir(jreOption)
// 加载jre目录下的所有jar包(jreDir/lib/*)
jreLibPath := filepath.Join(jreDir, "lib", "*")
self.bootClasspath = newWildcardEntry(jreLibPath)
// 加载jre目录下所有扩展jar包(jreDir/lib/ext/*)
jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
self.extClasspath = newWildcardEntry(jreExtPath)
}
// 解析用户类路径
func (self *Classpath) parseUserClasspath(cpOption string) {
// 如果用户没有提供-classpath/-cp选项,则使用当前目录作为用户类路径
if cpOption == "" {
cpOption = "."
}
self.userClasspath = newEntry(cpOption)
}
// 搜索class方法
func (self *Classpath) ReadClass(className string) ([]byte, Entry, error) {
//访问ReadClass方法只需传递类名,不用包含".class"后缀
className = className + ".class"
// 从bootClasspath搜索class文件
if data, entry, err := self.bootClasspath.readClass(className); err == nil {
return data, entry, err
}
// 从extClasspath搜索class文件
if data, entry, err := self.extClasspath.readClass(className); err == nil {
return data, entry, err
}
// 从userClasspath搜索class文件
return self.userClasspath.readClass(className)
}

测试

需要完善总的测试入口main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 模拟启动JVM
func startJVM(cmd *cmd.Cmd) {
// 获取Classpath
cp := classpath.Parse(cmd.XjreOption, cmd.CpOption)
fmt.Printf("classpath:%v class:%v args:%v\n",
cp, cmd.Class, cmd.Args)
// 将.替换成/(java.lang.String -> java/lang/String)
className := strings.Replace(cmd.Class, ".", "/", -1)
// 读取class
classData, _, err := cp.ReadClass(className)
if err != nil {
fmt.Printf("Could not find or load main class %s\n", cmd.Class)
return
}
fmt.Printf("class data:%v\n", classData)
}

1
2
3
4
5
6
cd /home/zhangjing/IdeaProjects/jvmgo/go
go install cn.didadu/jvmgo
# 没有传递-Xjre,会去读取$JAVA_HOME,成功打印出String.class的内容
./bin/jvmgo java.lang.String
# 尝试下传递错误的-Xjre,会输出找不到java.lang.String
./bin/jvmgo -Xjre /opt java.lang.String

搜索class文件的完整功能到此就实现好了,通过本章的学习,对jvm类的路径有了更清晰的认识。