写自己的JVM(0x8)

项目地址:

https://github.com/aprz512/write-your-own-jvm

数组介绍

数组在Java虚拟机中是个比较特殊的概念。为什么这么说呢?有下面几个原因:

首先,数组类和普通的类是不同的。普通的类从class文件中加载,但是数组类由Java虚拟机在运行时生成。数组的类名是左方括号([)+数组元素的类型描述符;数组的类型描述符就是类名本身。例如,int[]的类名是[Iint[][]的类名是[[IObject[]的类名是[Ljava/lang/Object;String[][]的类名是[[java/lang/String;等等。

其次,创建数组的方式和创建普通对象的方式不同。普通对象由new指令创建,然后由构造函数初始化。基本类型数组由newarray指令创建;引用类型数组由anewarray指令创建;另外还有一个专门的multianewarray指令用于创建多维数组。

最后,数组和普通对象存放的数据也是不同的。普通对象中存放的是实例变量,通过putfieldgetfield指令存取。数组对象中存放的则是数组元素,通过<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;
    }

测试

测试用例:

 

测试配置:

 

测试输出:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二手的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值