这节课需要了解的部分是类加载
javapublic class Main
{
public static void main(String[] args) {
System.out.println("Hello World");
}
}
上面是一个最简单的Java程序,一个Hello World!,试想如果我们有一个执行字节码的Java虚拟机,我们能直接加载这个类运行吗?
答案是不行,为什么呢?因为这个程序还用到了String类和System这两个类,如果直接加载Main肯定会报找不到xxx,我们需要知道的就是JVM是怎么找到这些类的。
主流的JVM把所有类分为三个路径
背过八股文的应该都有所耳闻,分别是启动类路径,扩展类路径和用户类路径,启动类一般在jre/lib,扩展类一般在jre/lib/ext
接下来我们要做的就是增加一个指定JRE路径的参数-Xjre,修改上节课部分代码
goflag.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,
类库是一个个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 许可协议。转载请注明出处!