动手实现自己的 JVM——Go!(ch02)
参考张秀宏老师的《自己动手写java虚拟机》
ch02代码结构
一、启动 Java 程序的过程
启动 Java 程序的过程大致可以分为以下几个阶段:
- 启动 JVM
当我们启动一个 Java 程序时,首先是启动 Java 虚拟机(JVM)。JVM 是运行 Java 程序的环境,它负责管理内存、加载类文件、执行字节码等任务。 - 加载主类
启动 JVM 后,接下来 JVM 会根据我们指定的主类(通常是通过命令行参数指定)加载该类。JVM 会查找主类的字节码文件(通常是.class
文件),并将其加载到内存中。 - 调用主类的
main
方法
主类加载完成后,JVM 会自动调用主类的main
方法作为程序的入口点,开始执行程序中的逻辑。
处理类加载的顺序
在加载我们自己的类之前,JVM 必须先加载其超类,确保类的继承结构能够正确解析和使用。例如,如果我们自己定义的类是继承自 java.lang.Object
的,JVM 必须先加载 Object
类。
此外,在调用主类的 main
方法之前,JVM 还需要加载一些基础类,如 java.lang.String
。这是因为 main
方法的签名通常为 public static void main(String[] args)
,其中 String
类型的数组 args
就需要先加载 String
类。
本节的目标
本节的核心目标是了解如何找到这些类并进行加载。我们不仅要加载我们的应用程序类,还需要确保基础类(如 Object
、String
等)在执行时已被正确加载。为了实现这一点,我们需要解决的问题是:
二、类路径分类
根据类路径的不同来源和作用,类路径可以分为以下几种类型。按照搜索顺序,类路径的分类如下图所示:
启动类(Bootstrap ClassLoader)
启动类路径(Bootstrap ClassPath)主要包括 Java 核心类库,包含了 JVM 启动所需的基础类,如 java.lang.*
包中的类。启动类一般位于 Java 安装目录的 jre/lib
目录下。它包含了 Java 的核心类库,JVM 在启动时首先加载这些类,以确保 Java 程序能够正确运行。
-
一般位于:
jre/lib/rt.jar
或类似路径。
扩展类(Extension ClassLoader)
扩展类路径(Extension ClassPath)包含了 JDK 中的一些扩展库,这些库可以通过 -Djava.ext.dirs
来指定。扩展类一般包含了对 Java 核心功能的扩展,如 javax.*
和 org.*
等包。
- 一般位于:
jre/lib/ext
目录。
用户类(Application ClassLoader)
用户类路径(Application ClassPath)是我们在编写 Java 程序时,直接引用的类文件所在的路径。它包含了用户编写的程序的 .class
文件及其他依赖的第三方库(如 JAR 文件)。在程序启动时,JVM 会按照指定的类路径(通过 -cp
或 -classpath
参数设置)来查找用户自定义的类。
- 一般位于: 用户指定的路径,通常是应用程序的
bin
目录或外部 JAR 文件。
修改 Cmd
类
首先,在第一章的基础上,我们需要修改 Cmd
类以支持新的命令行选项。具体的修改如下:
type Cmd struct {
helpFlag bool
versionFlag bool
cpOption string
XjreOption string // 新增的选项
class string
args []string
}
func parseCmd() *Cmd {
cmd := &Cmd{}
flag.Usage = printUsage
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") // 新增的选项,用于指定 JRE 路径
flag.Parse()
// 继续处理命令行参数...
}
在这个修改中,主要增加了一个新的选项 -Xjre
,用于指定 JRE 路径。这一修改使得命令行能够接收更多的参数,便于启动 Java 程序时指定不同的 JRE 路径。
三、具体编码实现
(1)类路径项的视线
设计模式——组合模式:接口实现与示例
在张宏秀老师的代码中,采用了组合模式来设计类路径的结构。组合模式通过将对象组合成树形结构,使得客户端可以一致地对待单个对象和组合对象。在 Go 语言中,组合模式的实现通常通过接口和结构体的组合来完成。
1. 接口定义:Entry
首先,我们定义了一个接口 Entry
,它规定了两个方法:
readClass(className string) ([]byte, Entry, error)
:用来读取指定类的字节内容。String()
:返回Entry
对象的字符串表示。
type Entry interface {
readClass(className string) ([]byte, Entry, error)
String() string
}
接口是 Go 中面向对象编程的核心,任何类型只要实现了接口中的所有方法,就自然地成为该接口的实现类型。Go 语言没有显式的 implements
关键字,而是通过“结构体方法集”来自动匹配接口。
2. 接口实现:不同类型的 Entry
根据类路径的不同类型(如普通目录、ZIP 文件、通配符路径等),我们提供了不同的 Entry
实现,以下是主要实现的概述。
newEntry
函数:根据路径创建合适的 Entry
类型
func newEntry(path string) Entry {
// 分隔符用于分隔路径,适应操作系统的不同
if strings.Contains(path, pathListSeparator) {
return newCompositeEntry(path) // 处理多个路径组成的复合路径
}
if strings.HasSuffix(path, "*") {
return newWildcardEntry(path) // 处理通配符路径
}
if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
return newZipEntry(path) // 处理 JAR 或 ZIP 文件
}
return newDirEntry(path) // 处理普通目录
}
在 newEntry
函数中,我们根据不同的路径形式,返回不同的 Entry
实现:
newCompositeEntry(path)
:用于处理多个路径组合的情况,可能会是一个包含多个路径的复合路径。newWildcardEntry(path)
:用于处理通配符路径(如*.jar
),这通常表示可以匹配多个文件。newZipEntry(path)
:用于处理 JAR 或 ZIP 文件路径。newDirEntry(path)
:用于处理普通的目录路径。
每个 Entry
类型(如 ZipEntry
、DirEntry
等)都会实现 Entry
接口中的方法(readClass
和 String
),从而形成了一个灵活的、可以扩展的设计。
3. 示例:如何在 Go 中实现接口
下面,我们以 ZipEntry
作为示例,展示如何在 Go 中实现接口。
ZipEntry
实现 Entry
接口
package classpath
import (
"archive/zip"
"fmt"
"io"
"path/filepath"
)
type ZipEntry struct {
absPath string
}
func newZipEntry(path string) *ZipEntry {
absPath, err := filepath.Abs(path)
fmt.Printf("absPath:%v\n", absPath)
if err != nil {
panic(err)
}
return &ZipEntry{absPath}
}
func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {
fmt.Println("调用了zip")
fmt.Printf("self.absPath=%s\n", self.absPath)
// 打开zip文件
r, err := zip.OpenReader(self.absPath)
if err != nil {
return nil, nil, fmt.Errorf("failed to open zip file: %v", err)
}
defer r.Close()
// 遍历压缩包中的文件
for _, file := range r.File {
if file.Name == className {
rc, err := file.Open()
if err != nil {
return nil, nil, fmt.Errorf("failed to open class file: %v", err)
}
defer rc.Close()
// 读取文件内容
data, err := io.ReadAll(rc)
if err != nil {
return nil, nil, fmt.Errorf("failed to read class file: %v", err)
}
// 返回文件内容、当前 entry 和 nil 错误
return data, self, nil
}
}
// 如果没有找到class文件,返回错误
return nil, nil, fmt.Errorf("class %s not found in zip", className)
}
func (self *ZipEntry) String() string {
return self.absPath
}
在这个例子中:
ZipEntry
结构体实现了Entry
接口。readClass
方法尝试从 ZIP 文件中读取指定的类文件内容。String
方法返回该ZipEntry
的路径信息。
4. 接口实现总结
Go 语言的接口实现是隐式的,这意味着我们不需要显式地声明一个类型实现了某个接口,只要该类型实现了接口中的所有方法,Go 会自动认为它实现了该接口。
Go 接口实现的特点:
- 隐式实现:无需显式声明类型实现了接口,接口方法集符合即可。
- 结构体与接口解耦:结构体不需要继承接口,而是通过方法集的匹配实现接口。
- 灵活性和扩展性:可以非常灵活地为新的类型提供接口实现,符合“面向接口编程”的思想。
组合模式通过接口和结构体的组合,实现了不同路径类型的处理。例如,通过 Entry
接口,我们统一管理了不同类型路径的读取逻辑(如文件路径、JAR 文件、目录等)。这种方式增强了代码的可扩展性和维护性。
当添加新的路径类型时,我们只需要实现 Entry
接口,而不需要修改现有的代码结构。这使得系统更加灵活且具有较高的可扩展性。
通过这样的设计,程序能够处理不同类型的类路径(包括普通文件、JAR 文件、目录、通配符等),并且对于客户端代码来说,无论是单个文件还是复合路径,它们的处理方式是统一的。
ps:如果需要总的代码,可以去我的github或者张宏秀老师的github扒拉哦:9lucifer/go_jvm: 根据张宏秀老师的著作编写的分章节jvm
(2)classpath类的实现
package classpath
import (
"fmt"
"os"
"path/filepath"
)
type ClassPath struct {
bootClasspath Entry
extClasspath Entry
userClasspath Entry
}
func Parse(jreOption, cpOption string) *ClassPath {
cp := &ClassPath{}
cp.parseBootAndExtClassPath(jreOption)
cp.parseUserClassPath(cpOption)
return cp
}
func (self *ClassPath) parseBootAndExtClassPath(jreOption string) {
jreDir := getJreDir(jreOption)
// jre/lib/*
jreLibPath := filepath.Join(jreDir, "lib", "*")
self.bootClasspath = newWildcardEntry(jreLibPath)
// jre/lib/ext/*
jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
self.extClasspath = newWildcardEntry(jreExtPath)
}
func getJreDir(jreOption string) string {
if jreOption != "" && exists(jreOption) {
return jreOption
}
if exists("./jre") {
return "./jre"
}
if jh := os.Getenv("JAVA_HOME"); jh != "" {
return filepath.Join(jh, "jre")
}
panic("can not find jre dir")
}
func exists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
func (self *ClassPath) String() string {
return self.userClasspath.String()
}
// className: fully/qualified/ClassName
func (self *ClassPath) ReadClass(className string) ([]byte, Entry, error) {
className = className + ".class"
// 1. 先检查 bootClasspath
data, entry, err := self.bootClasspath.readClass(className)
if err != nil {
fmt.Printf("在 bootClasspath 中未找到类 %s: %v\n", className, err)
} else {
return data, entry, nil // 如果找到类,直接返回
}
// 2. 如果 bootClasspath 中没有找到,继续检查 extClasspath
data, entry, err = self.extClasspath.readClass(className)
if err != nil {
fmt.Printf("在 extClasspath 中未找到类 %s: %v\n", className, err)
} else {
return data, entry, nil // 如果找到类,直接返回
}
// 3. 如果 extClasspath 中也没有找到,最后检查 userClasspath
data, entry, err = self.userClasspath.readClass(className)
if err != nil {
fmt.Printf("在 userClasspath 中未找到类 %s: %v\n", className, err)
return nil, nil, fmt.Errorf("类 %s 未找到", className)
}
// 如果找到类,返回结果
return data, entry, nil
}
func (self *ClassPath) parseUserClassPath(cpOption string) {
if cpOption == "" {
cpOption = "."
}
self.userClasspath = newEntry(cpOption)
}
代码解析
这段代码实现了类路径(ClassPath
)的解析和类加载功能,它的作用是帮助程序确定和加载 Java 类文件。具体来说,代码定义了一个 ClassPath
结构体,包含了三个不同的类路径(bootClasspath、extClasspath、userClasspath),并根据提供的路径来加载类文件。
ClassPath
结构体:bootClasspath
: 启动类路径,用于加载 JDK 自带的核心类库。extClasspath
: 扩展类路径,用于加载 JDK 的扩展库。userClasspath
: 用户类路径,用于加载用户自己编写的类。
Parse
函数:- 这是
ClassPath
的构造函数,用于解析启动类路径和用户类路径。它调用了parseBootAndExtClassPath
和parseUserClassPath
来设置类路径。
- 这是
parseBootAndExtClassPath
函数:- 这个函数根据 JRE 目录的路径(
jreOption
)来确定 boot 和 ext 类路径。 jreDir
是 JRE 的目录,如果指定了 JRE 路径则使用指定的路径,否则尝试使用默认路径。bootClasspath
和extClasspath
分别指向 JRE 中的lib
和lib/ext
目录,用于加载 Java 的核心类和扩展类。
- 这个函数根据 JRE 目录的路径(
getJreDir
函数:- 用来确定 JRE 目录的位置。首先会检查
jreOption
路径是否存在,其次检查当前目录下是否有./jre
目录,最后从环境变量JAVA_HOME
获取路径。
- 用来确定 JRE 目录的位置。首先会检查
exists
函数:- 检查一个路径是否存在。通过
os.Stat
来判断文件或目录是否存在。
- 检查一个路径是否存在。通过
ReadClass
函数:- 这个函数用于读取一个类的字节数据,按照以下顺序检查类路径:
- 首先在
bootClasspath
中查找。 - 如果没找到,则在
extClasspath
中查找。 - 如果依然没找到,最后检查
userClasspath
。
- 首先在
- 如果在某个路径中找到了类,返回类的字节数据;如果所有路径都未找到,返回错误。
- 这个函数用于读取一个类的字节数据,按照以下顺序检查类路径:
String
函数:- 返回用户类路径的字符串表示,通常用于调试输出。
parseUserClassPath
函数:- 用于解析用户类路径。如果没有指定用户类路径,则默认为当前目录
.
。
- 用于解析用户类路径。如果没有指定用户类路径,则默认为当前目录
(3)测试类——在之前的基础上修改
为了观感,我做了转16进制的处理
func startJvm(cmd *Cmd) {
fmt.Printf("cmd.class : %v\n", cmd.class)
cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
fmt.Printf("classpath: %v\n class:%v\n args:%v\n", cp, cmd.class, cmd.args)
className := strings.Replace(cmd.class, ".", "/", -1)
fmt.Printf("className:%v\n", className)
classDate, _, err := cp.ReadClass(className)
if err != nil {
// 在这里出错了
fmt.Printf("could not find the class \"%s\"\n", cmd.class)
return
}
// 将 classDate 转换为十六进制字符串
hexData := fmt.Sprintf("%x", classDate)
// 格式化输出:每行 8 个字节,每个字节之间用空格分隔
prettyHex := ""
for i := 0; i < len(hexData); i += 2 {
if i > 0 {
if i%64 == 0 { // 每 8 个字节换行
prettyHex += "\n"
} else {
prettyHex += " "
}
}
prettyHex += hexData[i : i+2]
}
fmt.Printf("classmame:%v \nclassDate is:\n%v\n", className, prettyHex)
}
四、测试代码
如果没有一次性成功运行,完全正常!我自己也遇到过很多次失败(哭)😭