https://github.com/aprz512/write-your-own-jvm
这一篇,我们主要是实现方法区(现在叫元数据区)里面的东西。方法区主要存放从class文件获取的类信息。此外,类变量也存放在方法区中。当Java虚拟机第一次使用某个类时,它会搜索类路径,找到相应的class文件,然后读取并解析class文件,把相关信息放进方法区。
放进方法区的东西有:
常量池
类信息
字段信息
方法信息
类加载器,超类,接口等等
其实这些信息在之前我们解析 class 文件的时候都遇到过。现在只不过我们换一种形式来表示这些信息。
之前我们已经可以找到 class 文件,并把其内容加载到内存中,对其解析成一个 ClassFile 的结构,但是 ClassFile 中的内容仍然无法直接在方法区使用,还需要进一步的转换。
我们先实现自己的 Class,为了与Java本身的类区分开,就加一个My前缀。
public class MyClass {
}
类的方法与字段结构比较像,我们写一个父类:
public class ClassMember {
protected int accessFlag;
protected String name;
protected String descriptor;
protected MyClass myClass;
}
再写 MyMethod 类:
public class MyMethod extends ClassMember {
}
public class MyField extends ClassMember {
}
MyClass 里面有一个 MyField 的数组,描述的是该类的所有字段。
MyClass 里面有一个 MyMethod 的数组,描述的是该类的所有方法。
MyField 里面有一个 MyClass 字段,表示这个字段是哪个类里面的。
MyMethod 里面有一个 MyClass 字段,表示这个方法是哪个类里面的。
建立了这样的双向关系,后面我们使用起来的时候会非常的方便。
MyClass,MyMethod,MyField的作用类似于Java原生的Class,Method,Field 的作用。
这些类的字段在这里被省略了,具体可以看源码。它们的字段的值在 ClassFile 里面都有,与解析Class时相比,就是多做了一层转换。比如,常量池里面的类信息,我们需要将它转换成具体的 MyClass 对象才能使用。
在Java8中,方法区被放到了 native 内存中。我们就不搞这个了,直接写。
运行时常量池主要存放两类信息:字面量(literal)和符号引用(symbolic reference)。
字面量包括整数、浮点数和字符串字面量;
符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。
看下面的例子:
public class Test04 {
private static final int a = 10;
private final String b = "20";
private void func(Runnable runnable) {
runnable.run();
}
}
看看编译后的常量池:
CONSTANT_Methodref_info:方法引用
CONSTANT_InterfaceMethodref_info:接口方法引用
CONSTANT_Fieldref_info:字段引用
CONSTANT_Class_info:类引用
至于其他的 CONSTANT_NameAndType_info 是用来间接描述字段与方法的,前面解析Class文件的时候介绍过。
CONSTANT_Integer_info:int 类型常量,类似的还有 double,float,String 等等。我们需要建立一个运行时常量池存放这些东西。
因为4种类型的符号引用有一些共性,所以仍然使用继承来减少重复代码。
public class SymRef {
/**
* 存放符号引用所在的运行时常量池指针,
* 这样就可以通过符号引用访问到运行时常量池,
* 进一步又可以访问到类数据。
*/
protected ConstantPool constantPool;
/**
* 存放类的完全限定名
*/
protected String className;
/**
* class字段缓存解析后的类结构体指针,这样类符号引用只需要解析一次就可以了,
* 后续可以直接使用缓存值。
*/
protected MyClass myClass;
}
接下来,实现 ClassRef:
public class ClassRef extends SymRef {
}
因为 SymRef 的字段完全够用了,所以不需要写额外的逻辑。
但是我们需要通过这个 ClassRef 拿到其对应的 MyClass 对象才行,所以我们需要定义一个 resolveClass 方法:
public MyClass getResolvedClass() {
if (this.myClass == null) {
myClass = resolveClassRef();
}
return myClass;
}
/**
* 解析常量池里面的类引用
*/
private MyClass resolveClassRef() {
MyClass mc = constantPool.getMyClass();
MyClass myClassRef = mc.getClassLoader().loadClass(className);
if (!myClassRef.isAccessibleTo(mc)) {
throw new RuntimeException("类[" + mc + "]无法访问到类[" + myClassRef + "]");
}
return myClassRef;
}
可以看到,我们是通过 ClassLoader 获得了对应的 MyClass。这是因为我们给定一个类名,ClassLoader 就能返回一个 MyClass, ClassLoader 里面是使用了一个 Map 来储存加载过的类。
跟解析 class 文件的时候一样,字段与方法结构类似,所以我们再抽象一个父类 MemberRef:
public class MemberRef extends SymRef
protected final String name;
protected final String descriptor;
}
里面主要是储存成员的 name 与 descriptor,就是成员的名字和类型描述。
接着实现 FieldRef:
public class FieldRef extends MemberRef {
private MyField myField;
}
FieldRef 的解析要稍微麻烦点:
public class FieldRef extends MemberRef {
...
public MyField getResolvedField() {
if (myField == null) {
myField = resolveFieldRef();
}
return myField;
}
private MyField resolveFieldRef() {
MyClass current = constantPool.getMyClass();
MyClass refClass = getResolvedClass();
MyField field = lookupField(refClass, name, descriptor);
if (field == null) {
throw new RuntimeException("java.lang.NoSuchFieldError");
}
if (!field.isAccessibleTo(current)) {
throw new RuntimeException("java.lang.IllegalAccessError");
}
return field;
}
/**
* 首先在C的字段中查找
* 如果找不到,在C的直接接口递归应用这个查找过程
* 如果还找不到的话,在C的超类中递归应用这个查找过程
* 如果仍然找不到,则查找失败
*/
private MyField lookupField(MyClass refClass, String name, String descriptor) {
MyField[] fields = refClass.getFields();
for (MyField field : fields) {
if (field.getDescriptor().equals(descriptor) && field.getName().equals(name)) {
return field;
}
}
MyClass[] interfaces = refClass.getInterfaces();
for (MyClass inter : interfaces) {
MyField interField = lookupField(inter, name, descriptor);
if (interField != null) {
return interField;
}
}
MyClass superClass = refClass.getSuperClass();
if (superClass != null) {
return lookupField(superClass, name, descriptor);
}
return null;
}
}
我们查找类中的字段时,是递归的往上查找的过程。子类找不到就找父类和接口里面的。
需要注意区分:当前类,符号引用所属的类。比如,一个 Test 类,它里面使用了 String,那么当前类就是 Test,符号引用类就是 String。
方法符号引用与字段符号引用类似,就贴一个引用解析方法:
private MyMethod resolveMethodRef() {
MyClass currentClass = constantPool.getMyClass();
MyClass resolvedClass = getResolvedClass();
// 暂不支持接口的静态方法和默认方法
if (resolvedClass.isInterface()) {
throw new MyJvmException("java.lang.IncompatibleClassChangeError");
}
MyMethod currentMethod = lookupMethod(resolvedClass, getName(), getDescriptor());
if (currentMethod == null) {
throw new MyJvmException("java.lang.NoSuchMethodError");
}
if (!currentMethod.isAccessibleTo(currentClass)) {
throw new MyJvmException("java.lang.IllegalAccessError");
}
return currentMethod;
}
private MyMethod lookupMethod(MyClass currentClass, String name, String descriptor) {
MyMethod lookupMethod = lookupMethodInClass(currentClass, name, descriptor);
if (lookupMethod == null) {
lookupMethod = lookupMethodInInterfaces(currentClass.getInterfaces(), name, descriptor);
}
return lookupMethod;
}
private MyMethod lookupMethodInInterfaces(MyClass[] interfaces, String name, String descriptor) {
for (MyClass inter : interfaces) {
for (MyMethod method : inter.getMethods()) {
if (method.getName().equals(name) && method.getDescriptor().equals(descriptor)) {
return method;
}
}
MyMethod method = lookupMethodInInterfaces(inter.getInterfaces(), name, descriptor);
if (method != null) {
return method;
}
}
return null;
}
public static MyMethod lookupMethodInClass(MyClass currentClass, String name, String descriptor) {
while (currentClass != null) {
MyMethod[] methods = currentClass.getMethods();
if (methods != null) {
for (MyMethod method : methods) {
if (method.getName().equals(name) && method.getDescriptor().equals(descriptor)) {
return method;
}
}
}
currentClass = currentClass.getSuperClass();
}
return null;
}
方法的查询过程与字段的查询过程是一致的。使用递归的方式,根据 name 和 getDescriptor 匹配就行。
private MyMethod resolveInterfaceMethodRef() {
MyClass currentClass = constantPool.getMyClass();
MyClass resolvedClass = getResolvedClass();
if (!resolvedClass.isInterface()) {
throw new MyJvmException("java.lang.IncompatibleClassChangeError");
}
MyMethod currentMethod = lookupMethodInInterfaces(new MyClass[]{resolvedClass}, getName(), getDescriptor());
if (currentMethod == null) {
throw new MyJvmException("java.lang.NoSuchMethodError");
}
if (!currentMethod.isAccessibleTo(currentClass)) {
throw new MyJvmException("java.lang.IllegalAccessError");
}
return currentMethod;
}
private MyMethod lookupMethodInInterfaces(MyClass[] interfaces, String name, String descriptor) {
for (MyClass inter : interfaces) {
for (MyMethod method : inter.getMethods()) {
if (method.getName().equals(name) && method.getDescriptor().equals(descriptor)) {
return method;
}
}
MyMethod method = lookupMethodInInterfaces(inter.getInterfaces(), name, descriptor);
if (method != null) {
return method;
}
}
return null;
}
将符号引用搞定之后,我们就可以创建运行时常量池了。与ClassFile的常量池相比,我们将 ref 常量具体化了。
private Constant createConstant(int tag, ConstantInfo info) {
switch (tag) {
。。。
case ConstantInfo.CONST_TAG_METHOD_REF:
ConstantMemberRefInfo methodRefInfo = (ConstantMemberRefInfo) info;
return new Constant(new MethodRef(this, methodRefInfo), tag);
。。。
}
}
根据不同的类型,创建不同的常量类型,其中CONST_TAG_METHOD_REF
类型,对应成 MethodRef
。这样当我们需要使用到运行时常量池里面的数据时,就可以直接通过它获取到它的 MyClass ,从而获取更多的信息。
类加载器除了需要搜索和读取class文件,还需要做下面的几件事:
验证
准备
解析
初始化
这几个步骤具体是做什么的,就不展开了,具体可以查询相关文档或者看书。我们并不准备实现一个非常标准的类加载器,所以只搞定准备与初始化就行了。解析过程我们在常量池里面就搞完了。验证阶段比较繁琐就不做了。
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
每个对象都有自己的字段,字段的个数我们可以计算出来。字段的储存我们也使用 Slot 来完成,因为字段有基本类型和引用类型,那么我们就需要计算出每个字段对应的 slot 的 id。
private void calcInstanceFieldSlotIds(MyClass myClass) {
MyClass superClass = myClass.getSuperClass();
int superSlotCount = superClass == null ? 0 : superClass.getInstanceSlotCount();
MyField[] fields = myClass.getFields();
int slotId = superSlotCount;
for (MyField field : fields) {
if (!field.isStatic()) {
field.setSlotId(slotId);
slotId++;
if (field.isLongOrDouble()) {
slotId++;
}
}
}
myClass.setInstanceSlotCount(slotId);
}
首先,我们获取该类父类的成员变量个数(不用递归,因为父类计算的时候也算上了它的父类的),然后以它为其实索引,计算自己的 myField 的 slot id。
假设,field 1 到 field 4 都是 int 类型,那么它们的 slot id 分别是 0, 1, 2, 3。
除了计算成员变量的 slot id,我们还需要计算类变量的 slot id。并且初始化它们的值。计算类变量的 slot id 很简单,给类变量(只final)赋值就稍微麻烦点,因为需要读取常量池里面的值:
private void initStaticFinalVar(MyClass myClass, MyField field) {
LocalVariableTable staticVars = myClass.getStaticVars();
ConstantPool constantPool = myClass.getConstantPool();
int constValueIndex = field.getConstValueIndex();
ConstantPool.Constant constant = constantPool.getConstant(constValueIndex);
int slotId = field.getSlotId();
if (constValueIndex > 0) {
switch (field.getDescriptor()) {
case "Z":
case "B":
case "C":
case "S":
case "I":
staticVars.setInt(slotId, (Integer) constant.value);
break;
case "J":
staticVars.setLong(slotId, (Long) constant.value);
break;
case "F":
staticVars.setFloat(slotId, (Float) constant.value);
break;
case "D":
staticVars.setDouble(slotId, (Double) constant.value);
break;
case "Ljava/lang/String;":
MyObject stringObject = MyString.create((String) constant.value, this);
staticVars.setRef(slotId, stringObject);
break;
default:
throw new NotImplementedException();
}
}
}
由于 static final 类型的变量里面储存了常量池的索引,所以我们读取后,然后更新其值即可。
最后,一个实例对象储存变量的结构如下:
在 new 一个对象的时候,init 方法会初始化 this 对象的 slot array 里面的值。同样的,该类初始化的时候,会调用其父类的 init 方法,就会初始化 super 对象的 slot array 里面的值。
初始化阶段就是执行类构造器<clinit>()
方法的过程。它需要我们手动的调用 clinit 方法。
对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
触发的情况比较多,我们可以先将初始化逻辑写好,后面实现那些指令/逻辑的时候直接调用就好了:
public class ClassInit {
public static void initMyClass(MyClass myClass, MyThread thread) {
myClass.setInitStarted(true);
scheduleClinit(myClass, thread);
initSuperClass(myClass, thread);
}
private static void initSuperClass(MyClass myClass, MyThread thread) {
if (!myClass.isInterface()) {
MyClass superClass = myClass.getSuperClass();
if (superClass != null && !superClass.isInitStarted()) {
initMyClass(superClass, thread);
}
}
}
private static void scheduleClinit(MyClass myClass, MyThread thread) {
MyMethod staticMethod = myClass.getStaticMethod("<clinit>", "()V");
if (staticMethod != null) {
StackFrame stackFrame = thread.newStackFrame(staticMethod);
thread.pushStackFrame(stackFrame);
}
}
}
代码中是实现了 10 条指令:
new指令用来创建类实例;
putstatic和getstatic指令用于存取静态变量;
putfield和getfield用于存取实例变量;
instanceof和checkcast指令用于判断对象是否属于某种类型;
ldc系列指令把运行时常量池中的常量推到操作数栈顶。
我们就详细说一下 new,putstatic,checkcast 吧。其他的可以看源代码或者看文档自己猜测实现。
new指令专门用来创建类实例。数组由专门的指令创建,我们后面实现数组和数组相关指令。
官方文档中,对 new 指令的描述:
将描述翻译成中文:(indexbyte1 <<
8) | indexbyte2 是一个无符号数,它是常量池的索引,索引项是一个类或者接口符号引用。
代码实现如下:
@Override
public void execute(StackFrame frame) {
ConstantPool constantPool = frame.getMyMethod().getMyClass().getConstantPool();
ConstantPool.Constant constant = constantPool.getConstant(operand);
ClassRef classRef = (ClassRef) constant.value;
MyClass refClass = classRef.getResolvedClass();
if (!refClass.isInitStarted()) {
frame.revertPc();
ClassInit.initMyClass(refClass, frame.getThread());
return;
}
if (refClass.isAbstract() || refClass.isInterface()) {
throw new MyJvmException("java.lang.InstantiationError");
}
MyObject myObject = refClass.newObject();
frame.getOperandStack().pushRef(myObject);
}
在执行 new 指令的时候,需要初始化该引用类,所以我们会将 pc 重置到指令该指令前,这样初始化引用类后,会重新的执行 new 指令。
核心逻辑很简单,就是创建一个 MyObject 对象,放到操作数栈栈顶即可。这个 MyObject 会与该引用类像关联,表示它是引用类的一个实例。
从这里看出,我们的JVM中,对象全是 MyObject,只不过它的成员变量表里面储存的东西不一样而已。
putstatic指令给类的某个静态变量赋值,它需要两个操作数。第一个操作数是uint16索引,来自字节码。通过这个索引可以从当前类的运行时常量池中找到一个字段符号引用
,解析这个符号引用就可以知道要给类的哪个静态变量赋值。第二个操作数是要赋给静态变量的值,从操作数栈中弹出。
代码实现:
@Override
public void execute(StackFrame frame) {
MyMethod myMethod = frame.getMyMethod();
ConstantPool constantPool = myMethod.getMyClass().getConstantPool();
ConstantPool.Constant constant = constantPool.getConstant(operand);
FieldRef fieldRef = (FieldRef) constant.value;
MyField field = fieldRef.getResolvedField();
MyClass fieldClass = field.getMyClass();
if (!fieldClass.isInitStarted()) {
frame.revertPc();
ClassInit.initMyClass(fieldClass, frame.getThread());
return;
}
if (!field.isStatic()) {
throw new MyJvmException("java.lang.IncompatibleClassChangeError");
}
// 如果是final字段,则实际操作的是静态常量,只能在类初始化方法中给它赋值。
if (field.isFinal()) {
if (myMethod.getMyClass() != fieldClass || !"<clinit>".equals(myMethod.getName())) {
throw new MyJvmException("java.lang.IllegalAccessError");
}
}
String descriptor = field.getDescriptor();
int slotId = field.getSlotId();
LocalVariableTable staticVars = fieldClass.getStaticVars();
OperandStack operandStack = frame.getOperandStack();
switch (descriptor.charAt(0)) {
case 'Z':
case 'B':
case 'C':
case 'S':
case 'I':
staticVars.setInt(slotId, operandStack.popInt());
break;
case 'F':
staticVars.setFloat(slotId, operandStack.popFloat());
break;
case 'J':
staticVars.setLong(slotId, operandStack.popLong());
break;
case 'D':
staticVars.setDouble(slotId, operandStack.popDouble());
break;
case 'L':
case '[':
staticVars.setRef(slotId, operandStack.popRef());
break;
default:
throw new NotImplementedException();
}
}
测试类:
public class Test06 {
public static int staticVar;
public int instanceVar;
public static void main(String[] args) {
int x = 32768; // ldc
Test06 myObj = new Test06(); // new
Test06.staticVar = x; // putstatic
x = Test06.staticVar; // getstatic
myObj.instanceVar = x; // putfield
x = myObj.instanceVar; // getfield
Object obj = myObj;
if (obj instanceof Test06) { // instanceof
myObj = (Test06) obj; // checkcast
System.out.println(myObj.instanceVar);
}
}
}
测试结果:
classpath: ./app/build/classes/java/main/
main class: write.your.own.jvm.test.Test06
args: []
32768
非常的完美,没啥问题!!!