编辑
2024-07-29
后端
00
请注意,本文编写于 285 天前,最后修改于 285 天前,其中某些信息可能已经过时。

这节课需要了解的部分是类加载

java
public class Main { public static void main(String[] args) { System.out.println("Hello World"); } }

上面是一个最简单的Java程序,一个Hello World!,试想如果我们有一个执行字节码的Java虚拟机,我们能直接加载这个类运行吗?

答案是不行,为什么呢?因为这个程序还用到了String类和System这两个类,如果直接加载Main肯定会报找不到xxx,我们需要知道的就是JVM是怎么找到这些类的。

主流的JVM把所有类分为三个路径

  • bootstrap classpath
  • extension classpath
  • user classpath

背过八股文的应该都有所耳闻,分别是启动类路径,扩展类路径和用户类路径,启动类一般在jre/lib,扩展类一般在jre/lib/ext

接下来我们要做的就是增加一个指定JRE路径的参数-Xjre,修改上节课部分代码

go
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") flag.Parse() .... type Cmd struct { helpFlag bool versionFlag bool cpOption string XjreOption string class string args []string }

主要是加了个参数指定Jre路径,然后按照刚刚我们的思路写代码,总共有三个预选的类路径抽象出来是下面这样

go
// 定义Classpath结构体,用于管理启动类路径、扩展类路径和用户类路径 type Classpath struct { // 启动类路径,用于存放Java核心库 bootClasspath Entry // 扩展类路径,用于存放Java扩展库 extClasspath Entry // 用户类路径,用于存放用户自定义的类和资源 userClasspath Entry }

Entry是代表类路径里面每一个项,打开jre\lib,

image.png

类库是一个个jar包组织的,所以我们肯定要有一个zip/jar的实现ZipEntry,其他实现分别是DirEntry,代表一个目录项处理,CompositeEntry,处理多路径,WildcardEntry,处理某个路径下所有jar包(模糊匹配)

看一下Entry

go
// 定义路径列表分隔符,根据操作系统不同,可能是冒号(Linux/Unix)或分号(Windows) const pathListSeparator = string(os.PathListSeparator) // 定义Entry接口,包含两个方法:readClass用于读取类文件,String用于返回Entry的字符串表示 type Entry interface { readClass(className string) ([]byte, Entry, error) String() string } // 根据路径参数选择构建对应的Entry实现 func newEntry(path string) Entry { // 如果路径包含路径列表分隔符,表示是多路径参数,例如 xxx;xxx;xxx; if strings.Contains(path, pathListSeparator) { return newCompositeEntry(path) } // 如果路径以通配符 "*" 结尾,表示要处理该目录下的所有文件,例如 /jre/* if strings.HasSuffix(path, "*") { return newWildcardEntry(path) } // 如果路径以 ".jar" 或 ".JAR" 或 ".zip" 或 ".ZIP" 结尾,表示要处理Jar包或Zip包 if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") || strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") { return newZipEntry(path) } // 如果以上条件都不满足,表示是单个目录路径 return newDirEntry(path) }

接下来看几个实现,DirEntry

go
// 定义DirEntry结构体,用于表示目录条目,包含一个字段absDir表示目录的绝对路径 type DirEntry struct { absDir string } // 创建一个新的DirEntry实例,将传入的路径转换为绝对路径 func newDirEntry(path string) *DirEntry { // 将传入的路径转换为绝对路径 absDir, err := filepath.Abs(path) // 如果转换过程中发生错误,抛出异常 if err != nil { panic(err) } // 返回一个新的DirEntry实例,包含转换后的绝对路径 return &DirEntry{absDir} } // 读取指定类名的类文件,返回类数据、DirEntry实例和错误信息 func (self *DirEntry) readClass(className string) ([]byte, Entry, error) { // 拼接目录绝对路径和类名,得到类的绝对路径 fileName := filepath.Join(self.absDir, className) // 读取类文件的内容 data, err := ioutil.ReadFile(fileName) // 返回类数据、DirEntry实例和错误信息 return data, self, err } // 返回DirEntry的字符串表示,即目录的绝对路径 func (self *DirEntry) String() string { return self.absDir }

ZipEntry

go
// 定义ZipEntry结构体,用于表示ZIP或JAR文件条目,包含一个字段absPath表示文件的绝对路径 type ZipEntry struct { absPath string } // 创建一个新的ZipEntry实例,将传入的路径转换为绝对路径 func newZipEntry(path string) *ZipEntry { // 将传入的路径转换为绝对路径 absPath, err := filepath.Abs(path) // 如果转换过程中发生错误,抛出异常 if err != nil { panic(err) } // 返回一个新的ZipEntry实例,包含转换后的绝对路径 return &ZipEntry{absPath} } // 读取指定类名的类文件,返回类数据、ZipEntry实例和错误信息 func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) { // 打开ZIP或JAR文件 r, err := zip.OpenReader(self.absPath) // 如果打开文件过程中发生错误,返回nil和错误信息 if err != nil { return nil, nil, err } // 确保在函数返回前关闭ZIP或JAR文件 defer r.Close() // 遍历ZIP或JAR文件中的所有文件 for _, f := range r.File { // 如果找到与类名匹配的文件 if f.Name == className { // 打开该文件 rc, err := f.Open() // 如果打开文件过程中发生错误,返回nil和错误信息 if err != nil { return nil, nil, err } // 确保在函数返回前关闭文件 defer rc.Close() // 读取文件内容 data, err := ioutil.ReadAll(rc) // 如果读取文件过程中发生错误,返回nil和错误信息 if err != nil { return nil, nil, err } // 返回类数据、ZipEntry实例和nil错误信息 return data, self, nil } } // 如果没有找到匹配的类文件,返回nil和错误信息 return nil, nil, errors.New("class not found: " + className) } // 返回ZipEntry的字符串表示,即ZIP或JAR文件的绝对路径 func (self *ZipEntry) String() string { return self.absPath }

CompositeEntry

go
// 定义CompositeEntry类型,它是一个Entry接口的切片,用于表示多个Entry的组合 type CompositeEntry []Entry // 创建一个新的CompositeEntry实例,根据路径列表创建多个Entry并添加到切片中 func newCompositeEntry(pathList string) CompositeEntry { // 创建一个空的Entry切片 compositeEntry := []Entry{} // 将路径列表按路径分隔符分割成多个路径 for _, path := range strings.Split(pathList, pathListSeparator) { // 为每个路径创建一个Entry entry := newEntry(path) // 将创建的Entry添加到切片中 compositeEntry = append(compositeEntry, entry) } // 返回包含多个Entry的CompositeEntry实例 return compositeEntry } // 读取指定类名的类文件,遍历所有Entry,尝试读取类文件,如果成功则返回类数据 func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) { // 遍历CompositeEntry中的所有Entry for _, entry := range self { // 尝试读取类文件 data, from, err := entry.readClass(className) // 如果读取成功,返回类数据、Entry实例和nil错误信息 if err == nil { return data, from, nil } } // 如果没有找到类文件,返回nil和错误信息 return nil, nil, errors.New("class not found: " + className) } // 返回CompositeEntry的字符串表示,将所有Entry的字符串表示用路径分隔符连接起来 func (self CompositeEntry) String() string { // 创建一个字符串切片,长度与CompositeEntry中的Entry数量相同 strs := make([]string, len(self)) // 遍历CompositeEntry中的所有Entry,将其字符串表示添加到字符串切片中 for i, entry := range self { strs[i] = entry.String() } // 将字符串切片中的所有字符串用路径分隔符连接起来,返回结果 return strings.Join(strs, pathListSeparator) }

WildcardEntry,由于这个和上面的思路都差不多,直接套用重写new方法

go
// 创建一个新的WildcardEntry实例,处理指定目录下的所有JAR文件 func newWildcardEntry(path string) CompositeEntry { // 去掉路径末尾的通配符 "*",得到基础目录 baseDir := path[:len(path)-1] // 创建一个空的Entry切片,用于存储所有JAR文件的Entry compositeEntry := []Entry{} // 定义walkFn函数,用于处理遍历到的每个文件或目录 walkFn := func(path string, info os.FileInfo, err error) error { // 如果遍历过程中发生错误,返回错误 if err != nil { return err } // 如果遍历到的是目录且不是基础目录,跳过该目录 if info.IsDir() && path != baseDir { return filepath.SkipDir } // 如果遍历到的文件是JAR文件(以 ".jar" 或 ".JAR" 结尾) if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") { // 为该JAR文件创建一个ZipEntry jarEntry := newZipEntry(path) // 将创建的ZipEntry添加到切片中 compositeEntry = append(compositeEntry, jarEntry) } // 返回nil表示继续遍历 return nil } // 使用filepath.Walk函数遍历基础目录及其所有子目录,对每个文件或目录调用walkFn函数 filepath.Walk(baseDir, walkFn) // 返回包含所有JAR文件Entry的CompositeEntry实例 return compositeEntry }

接下来就看看具体的加载逻辑,从命令行开始看

go
// 启动JVM函数,接受一个Cmd结构体指针作为参数 func startJVM(cmd *Cmd) { // 解析JRE选项和类路径选项,得到类路径加载对象 cp := classpath.Parse(cmd.XjreOption, cmd.cpOption) // 打印类路径、主类名和命令行参数 fmt.Printf("classpath:%v class:%v args:%v\n", cp, cmd.class, cmd.args) // 将主类名中的包名分隔符 "." 替换为文件路径分隔符 "/",转换为绝对路径形式 className := strings.Replace(cmd.class, ".", "/", -1) // 调用类路径加载对象的ReadClass方法,读取主类数据 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) }

我们先看看classpath.Parse

go
// 解析JRE选项和类路径选项,返回一个Classpath结构体指针 func Parse(jreOption, cpOption string) *Classpath { // 创建一个Classpath结构体实例 cp := &Classpath{} // 解析启动和扩展类路径,获取对应的加载对象 cp.parseBootAndExtClasspath(jreOption) // 解析用户类路径,获取对应的加载对象 cp.parseUserClasspath(cpOption) // 返回解析后的Classpath结构体指针 return cp } // 解析启动和扩展类路径,设置Classpath结构体的bootClasspath和extClasspath字段 func (self *Classpath) parseBootAndExtClasspath(jreOption string) { // 获取JRE目录路径 jreDir := getJreDir(jreOption) // 拼接JRE目录下的lib/*路径,表示启动类路径 jreLibPath := filepath.Join(jreDir, "lib", "*") // 创建一个WildcardEntry实例,表示启动类路径 self.bootClasspath = newWildcardEntry(jreLibPath) // 拼接JRE目录下的lib/ext/*路径,表示扩展类路径 jreExtPath := filepath.Join(jreDir, "lib", "ext", "*") // 创建一个WildcardEntry实例,表示扩展类路径 self.extClasspath = newWildcardEntry(jreExtPath) } // 获取JRE目录路径,优先使用jreOption,其次是当前目录下的jre目录,最后是JAVA_HOME环境变量 func getJreDir(jreOption string) string { // 如果jreOption不为空且路径存在,返回jreOption if jreOption != "" && exists(jreOption) { return jreOption } // 如果当前目录下的jre目录存在,返回该路径 if exists("./jre") { return "./jre" } // 如果JAVA_HOME环境变量存在,返回JAVA_HOME下的jre目录路径 if jh := os.Getenv("JAVA_HOME"); jh != "" { return filepath.Join(jh, "jre") } // 如果以上路径都不存在,抛出异常 panic("Can not find jre folder!") } // 检查指定路径是否存在 func exists(path string) bool { // 获取路径的文件信息,如果发生错误 if _, err := os.Stat(path); err != nil { // 如果错误表示路径不存在,返回false if os.IsNotExist(err) { return false } } // 路径存在,返回true return true }

代码很简单易懂,就是根据给出的路径创建对应的类路径项加载对象

然后是ReadClass方法

go
func (self *Classpath) ReadClass(className string) ([]byte, Entry, error) { className = className + ".class" // 拿到类名 if data, entry, err := self.bootClasspath.readClass(className); err == nil { // 现在启动类路径加载 return data, entry, err } if data, entry, err := self.extClasspath.readClass(className); err == nil { // 扩展类路径加载 return data, entry, err } return self.userClasspath.readClass(className) // 用户类路径 }

也很简单,这个顺序应该很熟悉了,很多八股都会将Java的类加载机制,双亲委托加载什么的,顺序不能乱,接下来就是一层层加载下去直到找到对应的class

当然我们这里只是加载到内存,还没做什么,包括解析什么的

第二章讲解到此

本文作者:yowayimono

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!