用Java手写jvm之实现查找class

评论: 0 浏览: 1848 ***新更新时间: 2个月前

写在前面

完成类加载器加载class的三阶段,加载,解析,初始化中的加载😀😀😀

源码 。

jvm想要运行class,是根据类全限定名称来从特定的位置基于类加载器来查找的,分别如下:

启动类加载器负责加载jre/lib目录
扩展类加载器负责加载jre/ext/lib目录
应用加载器负责加载应用程序classpath

不管是哪种类加载器,都要完成找到class查找这个工作。

具体类加载器的工作流程可以分为如下3个步骤:

1:加载
    根据类的全限定名称(包+类名称),定位并且读取字节码
2:链接(验证,准备,解析)
    验证字节码是否合法,给静态变量申请内存,符号引用(一个串)转直接引用(一个内存地址)
3:初始化
    执行初始化代码,如{}代码块,以及给静态变量赋值

本部分要来模拟的就是这里的1:加载。

1:程序

因为分别要从如下三个目录来查找class:

1:jre/lib
2:jre/ext/lib
3:应用classpath

不管是那个目录,用户在指定class所在位置时都可能使用不同的形式,可能如下:

1:一个目录,如d:\test\
2:一个jar,zip等,如d:\test\aa.jar,d:\test\aa.zip
3:通配符形式,如d:\test\*
4:也可能是以上三种的组合形式

为了抽象不同的文件形式,我们先来定义如下文件入口形式的接口:

public interface Entry {
    byte[] readClass(String className) throws IOException;
    static Entry create(String path) {
        //File.pathSeparator;路径分隔符(win\linux)
        if (path.contains(File.pathSeparator)) {
            return new CompositeEntry(path);
        }
        if (path.endsWith("*")) {
            return new WildcardEntry(path);
        }
        if (path.endsWith(".jar") || path.endsWith(".JAR") ||
                path.endsWith(".zip") || path.endsWith(".ZIP")) {
            return new ZipEntry(path);
        }
        return new DirEntry(path);
    }
}

其中抽象方法readClass用来完成读取字节码的工作,也就是本文要做的内容,静态方法create用来根据给定的路径生成对应的Entry对象,这里根据路径形式的不同我们需要定义Entry不同的子类,分别是目录形式的子类DirectoryEntry:

package com.dahuyou.find.clazz.entry.impl;
import com.dahuyou.find.clazz.entry.Entry;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
 * 目录形式的entry 如 d:\test
 */
public class DirectoryEntry implements Entry {
    private Path absolutePath;
    public DirectoryEntry(String path) {
        //获取***路径
        this.absolutePath = Paths.get(path).toAbsolutePath();
    }
    @Override
    public byte[] readClass(String className) throws IOException {
        return Files.readAllBytes(absolutePath.resolve(className));
    }
    @Override
    public String toString() {
        return this.absolutePath.toString();
    }
}

jar和zip形式的entry:

package com.dahuyou.find.clazz.entry.impl;
import com.dahuyou.find.clazz.entry.Entry;
import java.io.IOException;
import java.nio.file.*;
/**
 * jar,zip形式的entry
 */
public class JarOrZipEntry implements Entry {
    private Path absolutePath;
    public JarOrZipEntry(String path) {
        //获取***路径
        this.absolutePath = Paths.get(path).toAbsolutePath();
    }
    @Override
    public byte[] readClass(String className) throws IOException {
        try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
            return Files.readAllBytes(zipFs.getPath(className));
        }
    }
    @Override
    public String toString() {
        return this.absolutePath.toString();
    }
}

这里因为通配符形式的路径其实是组合形式的一种特殊写法,如d:\test\目录下有a.jar,b.jar,则组合形式就是d:\test\a.jar,d:\test\b.jar,而通配符形式就是d:\test\*,通配符形式是可以转换为组合形式的,只不过多了一个转换的工作而已,所以这里,我们单独定义组合形式子类,然后再继承组合形式定义通配符形式。

则,组合形式子类如下:

package com.dahuyou.find.clazz.entry.impl;
import com.dahuyou.find.clazz.entry.Entry;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
 * 一组路径的组合形式entry
 */
public class CompositeEntry implements Entry {
    private final List entryList = new ArrayList();
    public CompositeEntry(String pathList) {
        String[] paths = pathList.split(File.pathSeparator);
        for (String path : paths) {
            entryList.add(Entry.create(path));
        }
    }
    @Override
    public byte[] readClass(String className) throws IOException {
        for (Entry entry : entryList) {
            try {
                return entry.readClass(className);
            } catch (Exception ignored) {
                //ignored
            }
        }
        throw new IOException("class not found " + className);
    }
    @Override
    public String toString() {
        String[] strs = new String[entryList.size()];
        for (int i = 0; i  

通配符entry如下:

package com.dahuyou.find.clazz.entry.impl;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Collectors;
/**
 * 通配符entry,继承CompositeEntry,只需要将通配符转换为一组路径,就转换为组合entry了,
 * 所以这里直接继承组合entry,妙啊!!!
 */
public class WildcardEntry extends CompositeEntry {
    public WildcardEntry(String path) {
        super(toPathList(path));
    }
    private static String toPathList(String wildcardPath) {
        String baseDir = wildcardPath.replace("*", ""); // remove *
        try {
            return Files.walk(Paths.get(baseDir))
                    .filter(Files::isRegularFile)
                    .map(Path::toString)
                    .filter(p -> p.endsWith(".jar") || p.endsWith(".JAR"))
                    .collect(Collectors.joining(File.pathSeparator));
        } catch (IOException e) {
            return "";
        }
    }
}

UML图:

用Java手写jvm之实现查找class

又因为类***终其实是从classpath下查找的,所以我们还需要定义一个类来代表classpath,使用各种entry来完成具体查找class的工作:

package com.dahuyou.find.clazz.classpath;
import com.dahuyou.find.clazz.classpath.entry.Entry;
import com.dahuyou.find.clazz.classpath.entry.impl.WildcardEntry;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
 * classpath类(jre entry+jre ext entry+应用entry)
 */
public class Classpath {
    // jre路径的entry(jvm中启动类加载器使用)
    private Entry bootstrapClasspath;
    // jre扩展路径entry(jvm中扩展类加载器使用)
    private Entry extensionClasspath;
    // 应用class路径entry(jvm中app类加载器使用)
    private Entry userClasspath;
    public Classpath(String jreOption, String cpOption) {
        //启动类(D:\programs\javas\java1.8\jre\lib)&扩展类(D:\programs\javas\java1.8\jre\lib\ext)
        bootstrapAndExtensionClasspath(jreOption);
        //用户类 d:\test\target\classes
        parseUserClasspath(cpOption);
    }
    private void bootstrapAndExtensionClasspath(String jreOption) {
        String jreDir = getJreDir(jreOption);
        //..jre/lib/* jre核心类库初始化
        String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
        bootstrapClasspath = new WildcardEntry(jreLibPath);
        //..jre/lib/ext/* jre ext初始化
        String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
        extensionClasspath = new WildcardEntry(jreExtPath);
    }
    private static String getJreDir(String jreOption) {
        // 用户直接设置了jre目录 D:\programs\javas\java1.8\jre
        if (jreOption != null && Files.exists(Paths.get(jreOption))) {
            return jreOption;
        }
        if (Files.exists(Paths.get("./jre"))) {
            return "./jre";
        }
        String jh = System.getenv("JAVA_HOME");
        if (jh != null) {
            return Paths.get(jh, "jre").toString();
        }
        throw new RuntimeException("Can not find JRE folder!");
    }
    private void parseUserClasspath(String cpOption) {
        if (cpOption == null) {
            cpOption = ".";
        }
        userClasspath = Entry.create(cpOption);
    }
    public byte[] readClass(String className) throws Exception {
        className = className + ".class";
        // 先使用jre 的entry加载(对应jvm先通过启动类加载器加载)
        try {
            return bootstrapClasspath.readClass(className);
        } catch (Exception ignored) {
            //ignored
        }
        // jre的entry加载不到,使用ext对应的entry加载(对应jvm 启动类加载器加载不到,使用ext类加载器加载)
        try {
            return extensionClasspath.readClass(className);
        } catch (Exception ignored) {
            //ignored
        }
        // ***后通过用户类 entry加载(对应jvm***后猜使用app应用类加载器)
        return userClasspath.readClass(className);
    }
}

总结以上代码就是,为了对应不同形式的class,即不同入口形式的class,定义了entry接口,并根据不同的入口形式定义了对应的entry实现,然后定义了classpath类,该类分别针对jre,jre ext,用户类,定义了对应的entry,这样的当需要查找某个class时,就可以按照顺序分别从jre entry->jre ext entry->用户entry来查找了,可参考如下图:

用Java手写jvm之实现查找class

接着我们还需要定义接收命令行参数的类,这个如果你看过前面的文章就比较熟悉了,这里贴下源码:

package com.dahuyou.find.clazz.cmd;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import java.util.List;
public class Cmd {
    @Parameter(names = {"-?", "-help"}, description = "print help message", order = 3, help = true)
    boolean helpFlag = false;
    @Parameter(names = "-version", description = "print version and exit", order = 2)
    boolean versionFlag = false;
    @Parameter(names = {"-cp", "-classpath"}, description = "classpath", order = 1)
    String classpath;
    @Parameter(names = "-Xthejrepath", description = "path to jre", order = 4)
    String thejrepath;
    // 要查找的目标类
    @Parameter(names = "-Xthetargetclazz", description = "path to jre", order = 4)
    public String thetargetclazz;
    @Parameter(description = "main class and args")
    List mainClassAndArgs;
    boolean ok;
    String getMainClass() {
        return mainClassAndArgs != null && !mainClassAndArgs.isEmpty()
                ? mainClassAndArgs.get(0)
                : null;
    }
    List getAppArgs() {
        return mainClassAndArgs != null && mainClassAndArgs.size() > 1
                ? mainClassAndArgs.subList(1, mainClassAndArgs.size())
                : null;
    }
    static Cmd parse(String[] argv) {
        Cmd args = new Cmd();
        JCommander cmd = JCommander.newBuilder().addObject(args).build();
        cmd.parse(argv);
        args.ok = true;
        return args;
    }
}

通过-Xthejrepath来指定jre路径,当然也可以不指定,程序也会读取JAVA_HOME环境变量配置。-Xthetargetclazz指定要查找的目标类。

定义主函数类:

package com.dahuyou.find.clazz;
import com.dahuyou.find.clazz.classpath.Classpath;
import com.dahuyou.find.clazz.cmd.Cmd;
import java.util.Arrays;
/**
 * program arguments:-Xthejrepath D:\programs\javas\java1.8/jre -Xthetargetclazz D:\test\itstack-demo-jvm-master\find-class-from-classpath\target\test-classes\com\dahuyou\find\clazz\test\ShowMeByteCode
 */
public class Main {
    public static void main(String[] args) {
        Cmd cmd = Cmd.parse(args);
        if (!cmd.ok || cmd.helpFlag) {
            System.out.println("Usage:  [-options] class [args...]");
            return;
        }
        if (cmd.versionFlag) {
            //注意案例测试都是基于1.8,另外jdk1.9以后使用模块化没有rt.jar
            System.out.println("java version \"1.8.0\"");
            return;
        }
        startJVM(cmd);
    }
    private static void startJVM(Cmd cmd) {
        // 创建classpath
        Classpath cp = new Classpath(cmd.thejrepath, cmd.classpath);
        System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
        //获取className
//        String className = cmd.getMainClass().replace(".", "/");
        try {
//            byte[] classData = cp.readClass(className);
            byte[] classData = cp.readClass(cmd.thetargetclazz.replace(".", "/"));
            System.out.println(Arrays.toString(classData));
            System.out.println("classData:");
            for (byte b : classData) {
                //16进制输出
                System.out.print(String.format("%02x", b & 0xff) + " ");
            }
        } catch (Exception e) {
            System.out.println("Could not find or load main class " + cmd.getMainClass());
            e.printStackTrace();
        }
    }
}

为了测试,我们来定义一个要查找的类:

package com.dahuyou.find.clazz.test;
public class ShowMeByteCode {
    public static void main(String[] args) {
    }
}

运行,首先配置参数:

用Java手写jvm之实现查找class

-Xthejrepath
D:\programs\javas\java1.8/jre
-Xthetargetclazz
D:\test\itstack-demo-jvm-master\find-class-from-classpath\target\test-classes\com\dahuyou\find\clazz\test\ShowMeByteCode

-Xthejrepath设置jre的路径,-Xthetargetclazz设置要查找的目标class,运行如下:

classpath:com.dahuyou.find.clazz.classpath.Classpath@bebdb06 class:null args:null
[-54, -2, -70, -66, 0, 0, 0, 52, 0, 20, 10, 0, 3, 0, 17, 7, 0, 18, 7, 0, 19, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 44, 76, 99, 111, 109, 47, 100, 97, 104, 117, 121, 111, 117, 47, 102, 105, 110, 100, 47, 99, 108, 97, 122, 122, 47, 116, 101, 115, 116, 47, 83, 104, 111, 119, 77, 101, 66, 121, 116, 101, 67, 111, 100, 101, 59, 1, 0, 4, 109, 97, 105, 110, 1, 0, 22, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 4, 97, 114, 103, 115, 1, 0, 19, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 19, 83, 104, 111, 119, 77, 101, 66, 121, 116, 101, 67, 111, 100, 101, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 42, 99, 111, 109, 47, 100, 97, 104, 117, 121, 111, 117, 47, 102, 105, 110, 100, 47, 99, 108, 97, 122, 122, 47, 116, 101, 115, 116, 47, 83, 104, 111, 119, 77, 101, 66, 121, 116, 101, 67, 111, 100, 101, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 1, 0, 4, 0, 5, 0, 1, 0, 6, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 8, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 9, 0, 10, 0, 0, 0, 9, 0, 11, 0, 12, 0, 1, 0, 6, 0, 0, 0, 43, 0, 0, 0, 1, 0, 0, 0, 1, -79, 0, 0, 0, 2, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 6, 0, 8, 0, 0, 0, 12, 0, 1, 0, 0, 0, 1, 0, 13, 0, 14, 0, 0, 0, 1, 0, 15, 0, 0, 0, 2, 0, 16]
classData:
ca fe ba be 00 00 00 34 00 14 0a 00 03 00 11 07 00 12 07 00 13 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 00 04 74 68 69 73 01 00 2c 4c 63 6f 6d 2f 64 61 68 75 79 6f 75 2f 66 69 6e 64 2f 63 6c 61 7a 7a 2f 74 65 73 74 2f 53 68 6f 77 4d 65 42 79 74 65 43 6f 64 65 3b 01 00 04 6d 61 69 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 13 53 68 6f 77 4d 65 42 79 74 65 43 6f 64 65 2e 6a 61 76 61 0c 00 04 00 05 01 00 2a 63 6f 6d 2f 64 61 68 75 79 6f 75 2f 66 69 6e 64 2f 63 6c 61 7a 7a 2f 74 65 73 74 2f 53 68 6f 77 4d 65 42 79 74 65 43 6f 64 65 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 00 21 00 02 00 03 00 00 00 00 00 02 00 01 00 04 00 05 00 01 00 06 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 07 00 00 00 06 00 01 00 00 00 03 00 08 00 00 00 0c 00 01 00 00 00 05 00 09 00 0a 00 00 00 09 00 0b 00 0c 00 01 00 06 00 00 00 2b 00 00 00 01 00 00 00 01 b1 00 00 00 02 00 07 00 00 00 06 00 01 00 00 00 06 00 08 00 00 00 0c 00 01 00 00 00 01 00 0d 00 0e 00 00 00 01 00 0f 00 00 00 02 00 10 
Process finished with exit code 0

可以使用sublime等工具打开class,查看十六进制字节码对比程序输出结果:

用Java手写jvm之实现查找class

写在后面

参考文章列表

【Java 基础篇】Java类加载器详解 。

jvm之启动参数 。


发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

点击启动AI问答
Draggable Icon