写在前面
本文来尝试模拟类加载器加载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
:
运行: