项目地址:
https://github.com/aprz512/write-your-own-jvm
数组介绍
数组在Java虚拟机中是个比较特殊的概念。为什么这么说呢?有下面几个原因:
首先,数组类和普通的类是不同的。普通的类从class文件中加载,但是数组类由Java虚拟机在运行时生成。数组的类名是左方括号([)
+数组元素的类型描述符;数组的类型描述符就是类名本身。例如,int[]
的类名是[I
,int[][]
的类名是[[I
,Object[]
的类名是[Ljava/lang/Object;
,String[][]
的类名是[[java/lang/String;
等等。
其次,创建数组的方式和创建普通对象的方式不同。普通对象由new指令创建,然后由构造函数初始化。基本类型数组由newarray
指令创建;引用类型数组由anewarray
指令创建;另外还有一个专门的multianewarray
指令用于创建多维数组。
最后,数组和普通对象存放的数据也是不同的。普通对象中存放的是实例变量,通过putfield
和getfield
指令存取。数组对象中存放的则是数组元素,通过<t>aload
和<t>astore
系列指令按索引存取。其中可以是a、b、c、d、f、i、l或者s,分别用于存取引用、byte、char、double、float、int、long或short类型的数组。另外,还有一个arraylength
指令,用于获取数组长度。
创建数组的Class
一般来说,一个 MyClass 实例对应一个 class 文件,但是数组的 Class 是没有对应的 class 文件的。所以我们直接创建一个 MyClass 对象,给它设置一个 className 就好了。
我们实现一下 ANewArray 指令,先看例子:
String[] argss = new String[4];
看看编译后的字节码:
anewarray 后面跟了一个类名 String。表示需要创建String类型的数组,数组的大小由前一条指令给出,也就是4。
看看代码实现:
@Override
public void execute(StackFrame frame) {
int count = frame.getOperandStack().popInt();
if (count < 0) {
throw new MyJvmException("java.lang.NegativeArraySizeException");
}
ConstantPool constantPool = frame.getMyMethod().getMyClass().getConstantPool();
// must be a symbolic reference to a class, array, or interface type.
ConstantPool.Constant constant = constantPool.getConstant(operand);
ClassRef classRef = (ClassRef) constant.value;
MyClass resolvedClass = classRef.getResolvedClass();
MyClass arrayClass = resolvedClass.toArrayClass();
ArrayObject arrayObject = arrayClass.newArrayObject(count);
frame.getOperandStack().pushRef(arrayObject);
}
逻辑很简单,resolvedClass 就是上面例子中的 String 类。我们需要根据这个类名创建出数组的类名,具体的规则就是:
// [XXX -> [[XXX
// int -> [I
// XXX -> [LXXX;
有了类名后,使用 ClassLoader 加载一下,当然 ClassLoader 里面的逻辑也需要特殊处理:
private MyClass loadArrayClass(String name) {
MyClass myClass = MyClass.createArrayClass(name, this);
loadedClasses.put(name, myClass);
return myClass;
}
主要就是设置 MyClass 里面的一些字段:
public static MyClass createArrayClass(String name, MyClassLoader loader) {
MyClass myClass = new MyClass();
myClass.accessFlag = AccessFlag.ACC_PUBLIC;
myClass.thisClassName = name;
myClass.classLoader = loader;
myClass.initStarted = true;
myClass.superClass = loader.loadClass("java/lang/Object");
myClass.interfaces = new MyClass[]{
loader.loadClass("java/lang/Cloneable"),
loader.loadClass("java/io/Serializable"),
};
return myClass;
}
创建数组的Object
有了数组的 MyClass 之后,就需要创建对应的 MyObject 对象了,由于操作数组的指令需要改变对应 index 位置的值,所以我采用了数组来实现 ArrayObject:
public class ArrayObject extends MyObject {
private Object[] array;
}
<t>aload指令
<t>aload系列指令按索引取数组元素值,然后推入操作数栈。看一个例子,AALoad:
public class AALoad implements Instruction {
@Override
public void execute(StackFrame frame) {
int index = frame.getOperandStack().popInt();
ArrayObject arrayObject = (ArrayObject) frame.getOperandStack().popRef();
if (arrayObject == null) {
throw new MyJvmException("java.lang.NullPointerException");
}
checkIndex(index, arrayObject.getArrayLength());
frame.getOperandStack().pushRef((MyObject) arrayObject.getArrayElement(index));
}
}
先从操作数栈里面取出一个索引值,再从操作数栈里面取出一个数组对象,然后拿到数组对象[索引]的值,重新 push 到操作数栈里面。
<t>astore
<t>astore系列指令按索引给数组元素赋值。与aload系列指令是相反的逻辑就不贴代码了。
字符串
在class文件中,字符串是以MUTF8格式保存的。补充一个知识点,JNI里面也是 MUTF8 格式的。
前面我们加载了 String 的 class 文件,并且也可以创建出对应的 MyObject 对象,但是有一个问题,String 的一个 api 我们用不了,为啥呢?看一个例子:
String x = "sdfsdfsdf ";
x.trim();
看看编译后字节码:
ldc 指令会根据不同的常量池类型返回不同的类型值:
@Override
public void execute(StackFrame frame) {
OperandStack operandStack = frame.getOperandStack();
ConstantPool constantPool = frame.getMyMethod().getMyClass().getConstantPool();
MyClassLoader currentClassLoader = constantPool.getMyClass().getClassLoader();
ConstantPool.Constant constant = constantPool.getConstant(operand);
switch (constant.tag) {
case ConstantInfo.CONST_TAG_INTEGER:
operandStack.pushInt((Integer) constant.value);
break;
case ConstantInfo.CONST_TAG_FLOAT:
operandStack.pushFloat((Float) constant.value);
break;
case ConstantInfo.CONST_TAG_CLASS:
ClassRef classRef = (ClassRef) constant.value;
MyObject jClass = classRef.getResolvedClass().getJClass();
operandStack.pushRef(jClass);
break;
case ConstantInfo.CONST_TAG_STRING:
// string
MyObject stringObject = MyString.create((String) constant.value, currentClassLoader);
operandStack.pushRef(stringObject);
break;
default:
throw new NotImplementedException("tag = " + constant.tag);
}
}
当常量池索引的类型是 String 类型的时候,我们获取到的是一个 Java 类型的 String 类型,需要将它转换成 MyObject 类型。不过这里需要注意的是,一个 java 的 String 对象肯定是做了初始化的,比如代码 String s = “xxx”;
,s变量的 value 字段需要初始化:
当一个String对象被创建的时候,value 的值就已经固定了,我们需要模拟这个过程:
public static MyObject create(String real, MyClassLoader classLoader) {
MyObject cache = StringPool.getInstance().get(real);
if (cache != null) {
return cache;
}
// create String object
MyClass stringClass = classLoader.loadClass("java/lang/String");
MyObject stringObject = stringClass.newObject();
// create char array object for value field
MyClass charArrayClass = classLoader.loadClass("[C");
ArrayObject arrayObject = charArrayClass.newArrayObject(real.getBytes().length);
// copy value bytes
for (int i = 0; i < real.getBytes().length; i++) {
arrayObject.setArrayElement(i, (char) real.getBytes()[i]);
}
// set value field
stringObject.setRefFieldValue("value", "[C", arrayObject);
StringPool.getInstance().putString(real, stringObject);
return stringObject;
}
测试
测试用例:
测试配置:
测试输出: