用Java手写jvm之模拟类加载器加载class

写在前面

本文来尝试模拟类加载器加载class的过程。

1:程序

首先来定义类加载器类:

java 复制代码
/**
 * 类加载器
 * 正常应该有bootstrap,ext,app三个类加载器,这里简单起见,只用一个来模拟了
 */
public class ClassLoader {
    // bootstrap,ext,app 组合的classpath
    private Classpath classpath;
    // 类名->对应的Class类
    private Map<String, Class> classMap;

    public ClassLoader(Classpath classpath) {
        this.classpath = classpath;
        this.classMap = new HashMap<>();
    }

    public Class loadClass(String className) {
        Class clazz = classMap.get(className);
        if (null != clazz) return clazz;
        try {
            return loadNonArrayClass(className);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

        private Class loadNonArrayClass(String className) throws Exception {
        byte[] data = this.classpath.readClass(className);
        if (null == data) {
            throw new ClassNotFoundException(className);
        }
        // 1:加载,找到字节码并生成Class对象
        Class clazz = defineClass(data);
        // 2:链接 验证:有效性 准备:静态变量空间申请  解析:符号引用转直接引用
        link(clazz);
        // 3:初始化 调用init clinit方法
        init(clazz);
        return clazz;
    }

    private void init(Class clazz) {
        System.out.println("todo 初始化类:" + clazz.name);
    }

    private void link(Class clazz) {
        verify(clazz);
        prepare(clazz);
    }

    private void prepare(Class clazz) {
        calcInstanceFieldSlotIds(clazz);
        calcStaticFieldSlotIds(clazz);
        allocAndInitStaticVars(clazz);
    }
    // ...
}

方法defineClass(data)对应了加载阶段,link(clazz)对应了链接阶段(包含验证,准备,解析),init(clazz);对应了初始化。

为了更加全面的模拟这个过程,我们还自定义了Class类,和Method类,如下:

java 复制代码
public class Class {
    // 类访问修饰符
    public int accessFlags;
    // 类名称
    public String name;
    // 父类名称,因为Java是单继承多实现所以这里的父类只有1个
    public String superClassName;
    // 父接口名称们,因为Java是单继承多实现,所以这里的父接口是多个
    public String[] interfaceNames;
    public RunTimeConstantPool runTimeConstantPool;
    // 类的字段信息们
    public Field[] fields;
    // 类的方法信息们,包含有方法对应的指令码数组信息,本地变量表和操作数栈大小信息
    public Method[] methods;
    // 加载本类的类加载器
    public ClassLoader loader;
    // 父类对应的Class对象
    public Class superClass;
    public Class[] interfaces;

    public int instanceSlotCount;
    public int staticSlotCount;
    public Slots staticVars;

    // ...
}
java 复制代码
public class Method extends ClassMember {
    // 操作数栈大小
    public int maxStack;
    // 本地变量表大小
    public int maxLocals;
    // 指令码字节数组
    public byte[] code;
    // ...
}

/**
 * 类成员对象
 * 字段
 * 方法
 * 因为字段和方法有些共享的内容,比如访问修饰符,名称,所属的class等,所以定义该公共父类
 */
public class ClassMember {
    // 字段、方法的访问修饰符
    public int accessFlags;
    // 方法,字段的名称
    public String name;
    // 方法,字段类型的描述
    // 如private String name [Ljava/lang/String;
    // 如public void (String name) {} ([Ljava/lang/String;)V
    public String descriptor;
    // 所属的类对应的Class对象
    public Class clazz;

    // ...
}

核心就是这些,具体的还是需要投入时间来看源码,来多debug,接着写测试类:

java 复制代码
/**
 * -Xthejrepath     D:\programs\javas\java1.8/jre -Xthetargetclazz     D:\test\itstack-demo-jvm-master\tryy-too-simulate-classload-load-clazz\target\test-classes\org\itstack\demo\test\HelloWorld
 */
public class Main {

    public static void main(String[] args) {
        Cmd cmd = Cmd.parse(args);
        if (!cmd.ok || cmd.helpFlag) {
            System.out.println("Usage: <main class> [-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());
        System.out.printf("classpath:%s parsed class:%s \n", cp, cmd.thetargetclazz);
        //获取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) + " ");
            }*/
            // 创建类加载器准备加载类
            /**
             * 加载3个阶段
             * 1:加载
             *      找到字节码,并将其存储到原元空间(<=7方法区),然后该类,该类父类,父接口也加载并在堆中生成对应的Class对象
             * 2:链接
             *      验证:验证文件内容的合法性,如是否cafebabe打头,结构是否符合定义
             *      准备:主要是给静态变量申请内存空间,以及赋初始值,如int,short这种则给默认值0
             *      解析:符号引用(指向类或者方法的一个字符串)转换为直接引用(jvm的内存地址)
             * 3:初始化
             *      执行<init>,<clinit>方法,完成静态变量的赋值
             */
            ClassLoader classLoader = new ClassLoader(cp);
            String clazzName = cmd.thetargetclazz.replace(".", "/");
            Class mainClass = classLoader.loadClass(clazzName);
            Method mainMethod = mainClass.getMainMethod();
            new Interpreter(mainMethod);


            /*// 创建className对应的ClassFile对象
            ClassFile classFile = loadClass(clazzName, cp);
            MemberInfo mainMethod = getMainMethod(classFile);
            if (null == mainMethod) {
                System.out.println("Main method not found in class " + cmd.classpath);
                return;
            }
            // 核心重点代码:通过解释器来执行main方法
            new Interpreter(mainMethod);*/
        } catch (Exception e) {
            System.out.println("Could not find or load main class " + cmd.getMainClass());
            e.printStackTrace();
        }
    }

    /**
     * 获取main函数,这里我们要模拟是执行器执行main函数的过程,当然其他方法也是一样的!!!
     * @param classFile
     * @return
     */
    private static MemberInfo getMainMethod(ClassFile classFile) {
        if (null == classFile) return null;
        MemberInfo[] methods = classFile.methods();
        for (MemberInfo m : methods) {
            if ("main".equals(m.name()) && "([Ljava/lang/String;)V".equals(m.descriptor())) {
                return m;
            }
        }
        return null;
    }

    /**
     * 生成class文件对象
     * @param clazzName
     * @param cp
     * @return
     */
    private static ClassFile loadClass(String clazzName, Classpath cp) {
        try {
            // 获取类class对应的byte数组
            byte[] classData = cp.readClass(clazzName);
            return new ClassFile(classData);
        } catch (Exception e) {
            System.out.println("无法加载到类: " + clazzName);
            return null;
        }
    }

}

定义需要加载的类,加载之后会解析该类main函数的指令码并使用自定义的解释器来执行代码:

java 复制代码
package org.itstack.demo.test;

/**
 * -Xjre D:\programs\javas\java1.8/jre D:\test\itstack-demo-jvm-master\try-too-simulate-interpreter\target\test-classes\org\itstack\demo\test\HelloWorld
 */
public class HelloWorld {
    
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
}

配置program argument-Xthejrepath D:\programs\javas\java1.8/jre -Xthetargetclazz D:\test\itstack-demo-jvm-master\tryy-too-simulate-classload-load-clazz\target\test-classes\org\itstack\demo\test\HelloWorld:

运行:

写在后面

参考文章列表

第23讲 | 请介绍类加载过程,什么是双亲委派模型?

用Java手写jvm之模拟解释器执行指令码

相关推荐
P.H. Infinity8 分钟前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天12 分钟前
java的threadlocal为何内存泄漏
java
caridle24 分钟前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^29 分钟前
数据库连接池的创建
java·开发语言·数据库
苹果醋333 分钟前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花37 分钟前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端40 分钟前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan1 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
全栈开发圈1 小时前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫