泉缘泉 发表于 2024-8-6 00:17:54

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

写在前面

本文来实验模拟类加载器加载class的过程。
1:程序

首先来定义类加载器类:
/**
* 类加载器
* 正常应该有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类,如下:
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;

    // ...
}
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,接着写测试类:
/**
* -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 ");
            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函数的指令码并使用自定义的解释器来实行代码:
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:
https://i-blog.csdnimg.cn/direct/9c293368d0d24d7c81d7a93faa9a3131.png
运行:
https://i-blog.csdnimg.cn/direct/91a975699fb04204ae65e598c79c406e.png
写在后面

参考文章列表

第23讲 | 请介绍类加载过程,什么是双亲委派模型? 。
用Java手写jvm之模拟解释器实行指令码 。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 用Java手写jvm之模拟类加载器加载class