用Java手写JVM第六章——类和对象


我们来总结一下。第1章实现了命令行。第2章实现了类路径,可以找到class文件,并把数据加载到内存中。第3章实现了class文件解析,可以把class数据解析成一个ClassFile结构体。在第4章,我们初步实现了线程私有的运行时数据区,在此基础上,第5章实现了一个简单的解释器和150多条指令。这些指令主要是操作局部变量表和操作数栈、进行数学运算、比较运算和跳转控制等。

本章将实现线程共享的运行时数据区,包括方法区和运行时常量池。本章将进一步处理ClassFile结构体,把它加以转换,放进方法区以供后续使用。本章还会初步讨论类和对象的设计,实现一个简单的类加载器,并且实现类和对象相关的部分指令。

代码目录

ZYX-demo-jvm-04
├── pom.xml
└── src
    └── main
    │    └── java
    │        └── org.ZYX.demo.jvm
	│             ├── classfile
    │             │   ├── attributes  
    │             │   ├── constantpool 
    │             │   ├── ClassFile.java
    │             │   ├── ClassReader.java
    │             │   └── MemberInfo.java	
    │             ├── classpath
    │             │   ├── impl
    │             │   │   ├── CompositeEntry.java
    │             │   │   ├── DirEntry.java 
    │             │   │   ├── WildcardEntry.java 
    │             │   │   └── ZipEntry.java    
    │             │   ├── Classpath.java
    │             │   └── Entry.java
    │             ├── rtda
    │             │   ├── heap
    │             │   │   ├── constantpool
    │             │   │   └── methodarea
    │             │   ├── Frame.java
    │             │   ├── JmvStack.java
    │             │   ├── LocalVars.java
    │             │   ├── OperandStack.java
    │             │   ├── Slot.java
    │             │   └── Thread.java
    │             ├── instructions
    │             │   ├── base  
    │             │   ├── conparisons
    │             │   ├── constants
    │             │   ├── control
    │             │   ├── conversions
    │             │   ├── extended
    │             │   ├── loads
    │             │   ├── math
    │             │   ├── stack
    │             │   ├── stores
    │             │   └── Factory.java		    
    │             ├── Cmd.java
    │             ├── Interpret.java
    │             └── Main.java
    └── test
         └── java
             └── org.ZYX.demo.test
                 └── HelloWorld.java

一、方法区

方法区是运行时数据区的一块逻辑区域,由多个线程共享。方法区主要存放从class文件获取的类信息。此外,类变量也存放在方法区中。先来看看有哪些信息需要放进方法区。

1、类信息

使用一个Class类来表示将要放进方法区内的类,其代码如下:

public class Class {

    public int accessFlags;         //类访问标志;                         
    public String name;             //类名 ;                           
    public String superClassName;   //父类名;
    public String[] interfaceNames; //接口名;这三个都是完全限定名,具有java/lang/Object的形式;
    public RunTimeConstantPool runTimeConstantPool;   //存放运行时常量池指针;
    public Field[] fields;          //存放字段表;
    public Method[] methods;        //存放方法表;
    public ClassLoader loader;
    public Class superClass;
    public Class[] interfaces;
    public int instanceSlotCount;
    public int staticSlotCount;
    public Slots staticVars;
}    

继续编辑Class类,在其中定义Class()方法,用来把ClassFile结构体转换成
Class结构体,如下:

  public Class(ClassFile classFile) {
        this.accessFlags = classFile.accessFlags();
        this.name = classFile.className();
        this.superClassName = classFile.superClassName();
        this.interfaceNames = classFile.interfaceNames();
        this.runTimeConstantPool = new RunTimeConstantPool(this, classFile.constantPool());
        this.fields = new Field().newFields(this, classFile.fields());
        this.methods = new Method().newMethods(this, classFile.methods());
    }

Class()函数又调用了RunTimeConstantPool()、newFields()和newMethods(),这三个函数的代码将在后面的小节给出。

继续编辑Class类,在其中定义8个方法,用来判断某个访问标志是否被设置。代码如下:

 public boolean isPublic() {
        return 0 != (this.accessFlags & AccessFlags.ACC_PUBLIC);
    }

    public boolean isFinal() {
        return 0 != (this.accessFlags & AccessFlags.ACC_FINAL);
    }

    public boolean isSuper() {
        return 0 != (this.accessFlags & AccessFlags.ACC_SUPER);
    }

    public boolean isInterface() {
        return 0 != (this.accessFlags & AccessFlags.ACC_INTERFACE);
    }

    public boolean isAbstract() {
        return 0 != (this.accessFlags & AccessFlags.ACC_ABSTRACT);
    }

    public boolean isSynthetic() {
        return 0 != (this.accessFlags & AccessFlags.ACC_SYNTHETIC);
    }

    public boolean isAnnotation() {
        return 0 != (this.accessFlags & AccessFlags.ACC_ANNOTATION);
    }

    public boolean isEnum() {
        return 0 != (this.accessFlags & AccessFlags.ACC_ENUM);
    }

后面将要介绍的Field和Method类也有类似的方法

2、字段信息

字段和方法都属于类的成员,它们有一些相同的信息(访问标志、名字、描述符)。为了避免重复代码,创建一个ClassMember类来存放这些信息。其结构如下:

public class ClassMember {

    public int accessFlags;
    public String name;
    public String descriptor;
    public Class clazz;
    
    //从class文件中复制数据;
    public void copyMemberInfo(MemberInfo memberInfo) {
        this.accessFlags = memberInfo.accessFlags();
        this.name = memberInfo.name();
        this.descriptor = memberInfo.descriptor();
    }
}

ClassMember定义好了,我们再来看Field类,它比较简单,目前所有信息都是从ClassMember中继承过来的。代码如下:

public class Field extends ClassMember {

    public int constValueIndex;
    public int slotId;
    
    //根据class文件的字段信息创建字段表
    public Field[] newFields(Class clazz, MemberInfo[] cfFields) {
        Field[] fields = new Field[cfFields.length];
        for (int i = 0; i < cfFields.length; i++) {
            fields[i] = new Field();
            fields[i].clazz = clazz;
            fields[i].copyMemberInfo(cfFields[i]);
        }
        return fields;
    }

3、方法信息

方法信息稍微复杂一点,因为方法中有字节码。我们先定义一个Method类,其结构如下:

public class Method extends ClassMember {

    public int maxStack;   //操作数栈大小;
    public int maxLocals;  //局部变量表大小;
    public byte[] code;    //字节码;
    
    //根据class文件中的方法信息创建Method表;
    public Method[] newMethods(Class clazz, MemberInfo[] cfMethods) {
        Method[] methods = new Method[cfMethods.length];
        for (int i = 0; i < cfMethods.length; i++) {
            methods[i] = new Method();
            methods[i].clazz = clazz;
            methods[i].copyMemberInfo(cfMethods[i]);
            methods[i].copyAttributes(cfMethods[i]);
        }
        return methods;
    }     

4、其他信息

Class类还有几个字段没有说明。loader字段存放类加载器指针,superClass和interfaces字段存放类的超类和接口指针,这三个字段将在6.3节介绍。staticSlotCount和instanceSlotCount字段分别存放类变量和实例变量占据的空间大小,staticVars字段存放静态变量,这三个字段将在6.4节介绍。

二、运行时常量池

运行时常量池主要存放两类信息:字面量(literal)和符号引用(symbolic reference)。字面量包括整数、浮点数和字符串字面量;符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。

我们定义一个constantpool包,其中创建RunTimeConstantPool类。代码如下:

public class RunTimeConstantPool {

    private Class clazz;
    private Object[] constants;
    
    //核心逻辑就是把classfile.ConstantInfos[]转换成heap.Constants[]。
    public RunTimeConstantPool(Class clazz, ConstantPool constantPool) {
        int cpCount = constantPool.getConstantInfos().length;
        this.clazz = clazz;
        this.constants = new Object[cpCount];

        ConstantInfo[] constantInfos = constantPool.getConstantInfos();
        for (int i = 1; i < cpCount; i++) {
            ConstantInfo constantInfo = constantInfos[i];

            switch (constantInfo.tag()) {
            
                //最简单的是int或float型常量,直接取出常量值,放进constants中即可;
                case ConstantInfo.CONSTANT_TAG_INTEGER:
                    ConstantIntegerInfo integerInfo = (ConstantIntegerInfo) constantInfo;
                    this.constants[i] = integerInfo.value();
                    break;
                case ConstantInfo.CONSTANT_TAG_FLOAT:
                    ConstantFloatInfo floatInfo = (ConstantFloatInfo) constantInfo;
                    this.constants[i] = floatInfo.value();
                    break;
                
                /*如果是long或double型常量,也是直接提取常量值放进constants中;
                但是要注意,这两种类型的常量在常量池中都是占据两个位置,所以索引要特殊               处理,   
                */ 
                case ConstantInfo.CONSTANT_TAG_LONG:
                    ConstantLongInfo longInfo = (ConstantLongInfo) constantInfo;
                    this.constants[i] = longInfo.value();
                    i++;
                    break;
                case ConstantInfo.CONSTANT_TAG_DOUBLE:
                    ConstantDoubleInfo doubleInfo = (ConstantDoubleInfo) constantInfo;
                    this.constants[i] = doubleInfo.value();
                    i++;
                    break;
                
                //如果是字符串常量,直接取出字符串,放进constants中
                case ConstantInfo.CONSTANT_TAG_STRING:
                    ConstantStringInfo stringInfo = (ConstantStringInfo) constantInfo;
                    this.constants[i] = stringInfo.string();
                    break;

                //还剩下4种类型的常量需要处理,分别是类、字段、方法和接口方法的符号引用。
                case ConstantInfo.CONSTANT_TAG_CLASS:
                    ConstantClassInfo classInfo = (ConstantClassInfo) constantInfo;
                    this.constants[i] = ClassRef.newClassRef(this, classInfo);
                    break;
                case ConstantInfo.CONSTANT_TAG_FIELDREF:
                    this.constants[i] = FieldRef.newFieldRef(this, (ConstantFieldRefInfo) constantInfo);
                    break;
                case ConstantInfo.CONSTANT_TAG_INTERFACEMETHODREF:
                    this.constants[i] = InterfaceMethodRef.newInterfaceMethodRef(this, (ConstantInterfaceMethodRefInfo) constantInfo);
                    break;
                case ConstantInfo.CONSTANT_TAG_METHODREF:
                    this.constants[i] = MethodRef.newMethodRef(this, (ConstantMethodRefInfo) constantInfo);
                    break;
                default:
            }
        }
    }

    public Class getClazz() {
        return clazz;
    }

    public void setClazz(Class clazz) {
        this.clazz = clazz;
    }

    public Object getConstants(int idx) {
        return constants[idx];
    }

}

getConstants()方法根据索引返回常量。

RunTimeConstantPool()方法数把class文件中的常量池转换成运行时常量池,具体详见上面代码注释。

1、类符号引用

因为4种类型的符号引用有一些共性,所以我们创建SymRef类,通过继承来减少重复代码。代码如下:

public class SymRef {

    public RunTimeConstantPool runTimeConstantPool; //放符号引用所在的运行时常量池;
    public String className; //存放类的完全限定名;
    public Class clazz;  //解析后的类结构体;

    public Class resolvedClass() {
        if (null != this.clazz) return this.clazz;
        Class d = this.runTimeConstantPool.getClazz();
        Class c = d.loader.loadClass(this.className);;
        this.clazz = c;
        return this.clazz;
    }

}

class字段缓存解析后的类结构体指针,这样类符号引用只需要解析一次就可以了,后续可以直接使用缓存值。对于类符号引用,只要有类名,就可以解析符号引用。对于字段,首先要解析类符号引用得到类数据,然后用字段名和描述符查找字段数据。方法符号引用的解析过程和字段符号引用类似。

好了,现在我们来定义ClassRef类。代码如下:

public class ClassRef extends SymRef {
       
       //newClassRef()函数根据class文件中存储的类常量创建ClassRef实例;
       public static ClassRef newClassRef(RunTimeConstantPool runTimeConstantPool, ConstantClassInfo classInfo) {
        ClassRef ref = new ClassRef();
        ref.runTimeConstantPool = runTimeConstantPool;
        ref.className = classInfo.name();
        return ref;
    }

}

2、字段符号引用

在前面的方法区中,定义了ClassMember类来存放字段和方法共有的信息。类似地,本节定义MemberRef类来存放字段和方法符号引用共有的信息。代码如下

public class MemberRef extends SymRef {

    public String name;
    public String descriptor;
    
    //从class文件内存储的字段或方法常量中提取数据;
    public void copyMemberRefInfo(ConstantMemberRefInfo refInfo){
        this.className = refInfo.className();
        Map<String, String> map = refInfo.nameAndDescriptor();
        this.name = map.get("name");
        this.descriptor = map.get("_type");
    }

    public String name(){
        return this.name;
    }

    public String descriptor(){
        return this.descriptor;
    }

}

这时我们可能就有疑问,为什么需要描述符。在Java中,我们并不能在同一个类中定义名字相同,但类型不同的两个字段。其实,这只是Java语言的限制,而不是Java虚拟机规范的限制。也就是说,站在Java虚拟机的角度,一个类是完全可以有多个同名字段的,只要它们的类型互不相同就可以。

MemberRef定义好了,我们来定义FieldRef类,代码如下:

public class FieldRef extends MemberRef {

    private Field field;//缓存后的字段指针;
    
    //创建FieldRef实例;
    public static FieldRef newFieldRef(RunTimeConstantPool runTimeConstantPool, ConstantFieldRefInfo refInfo) {
        FieldRef ref = new FieldRef();
        ref.runTimeConstantPool = runTimeConstantPool;
        ref.copyMemberRefInfo(refInfo);
        return ref;
    }
}

字段符号引用的解析将在第五小节讨论。

3、方法符号引用

我们来定义一个定义MethodRef类,代码和字段的差不多,如下所示:

public class MethodRef extends MemberRef {

    private Method method;

    public static MethodRef newMethodRef(RunTimeConstantPool runTimeConstantPool, ConstantMethodRefInfo refInfo){
       MethodRef ref = new MethodRef();
       ref.runTimeConstantPool = runTimeConstantPool;
       ref.copyMemberRefInfo(refInfo);
       return ref;
    }
}

方法符号引用的解析将在第7章讨论方法调用时详细介绍。

4、接口方法符号引用

我们来定义一个定义InterfaceMethodRef类,代码也和前面差不多,如下:

public class InterfaceMethodRef extends MemberRef {

    private Method method;

    public static InterfaceMethodRef newInterfaceMethodRef(RunTimeConstantPool runTimeConstantPool, ConstantInterfaceMethodRefInfo refInfo) {
        InterfaceMethodRef ref = new InterfaceMethodRef();
        ref.runTimeConstantPool = runTimeConstantPool;
        ref.copyMemberRefInfo(refInfo);
        return ref;
    }
}

接口方法符号引用的解析同样会在第7章详细介绍。

到此为止,所有的符号引用都已经定义好了,它们的继承结构如图:

符号引用结构体继承关系图

三、类加载器

接着我们将初步实现一个简单的类加载器,后面会慢慢进行扩展。

我们先定义一个ClassLoader类,代码如下:

public class ClassLoader {

    private Classpath classpath;//依赖cp来搜索和读取class文件;
    
    /*classMap字段记录已经加载的类数据,key是类的完全限定名。
    在前面讨论中,方法区一直只是个抽象的概念,
    现在可以把classMap字段当作方法区的具体实现。
    */
    private Map<String, Class> classMap;

    public ClassLoader(Classpath classpath) {
        this.classpath = classpath;
        this.classMap = new HashMap<>();
    }
}

然后是它的LoadClass()方法,把类数据加载到方法区,代码如下:

 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;
    }

先查找classMap,看类是否已经被加载。如果是,直接返回类数据,否则调用loadNonArrayClass()方法加载类。

数组类和普通类有很大的不同,它的数据并不是来自class文件,而是由Java虚拟机在运行期间生成。本章暂不考虑数组类的加载,留到第8章详细讨论。

loadNonArrayClass()方法的代码如下:


    private Class loadNonArrayClass(String className) throws Exception {
        byte[] data = this.classpath.readClass(className);
        if (null == data) {
            throw new ClassNotFoundException(className);
        }
        Class clazz = defineClass(data);
        link(clazz);
        return clazz;
    }

可以看到,类的加载大致可以分为三个步骤:首先找到class文件并把数据读取到内存;然后解析class文件,生成虚拟机可以使用的类数据,并放入方法区;最后进行链接。下面分别讨论这三步。

1、readClass()

readClass()方法只是调用了Classpath的readClass()方法,并进行了错误处理。需要解释一下它的返回值。

2、defineClass()

defineClass()方法的代码如下:

    private Class defineClass(byte[] data) throws Exception {
        Class clazz = parseClass(data);
        clazz.loader = this;
        resolveSuperClass(clazz);
        resolveInterfaces(clazz);
        this.classMap.put(clazz.name, clazz);
        return clazz;
    }

defineClass()方法首先调用parseClass()函数把class文件数据转换成Class结构体。Class结构体的superClass和interfaces字段存放超类名和直接接口表,这些类名其实都是符号引用。调用resolveSuperClass()和resolveInterfaces()函数解析这些类符号引用。下面是parseClass()函数的代码:

 private Class parseClass(byte[] data) {
        ClassFile classFile = new ClassFile(data);
        return new Class(classFile);
    }

resolveSuperClass()函数的代码如下:


    private void resolveSuperClass(Class clazz) throws Exception {
        if (!clazz.name.equals("java/lang/Object")) {
            clazz.superClass = clazz.loader.loadClass(clazz.superClassName);
        }
    }

除java.lang.Object以外,所有的类都有且仅有一个父类。因此,除非是Object类,否则需要递归调用LoadClass()方法加载它的超类。与此类似,resolveInterfaces()函数递归调用LoadClass()方法加载类的每一个直接接口,代码如下:

    private void resolveInterfaces(Class clazz) throws Exception {
        int interfaceCount = clazz.interfaceNames.length;
        if (interfaceCount > 0) {
            clazz.interfaces = new Class[interfaceCount];
            for (int i = 0; i < interfaceCount; i++) {
                clazz.interfaces[i] = clazz.loader.loadClass(clazz.interfaceNames[i]);
            }
        }
    }

3、link()

类的链接分为验证和准备两个必要阶段,link()方法的代码如下:

  private void link(Class clazz) {
        verify(clazz);
        prepare(clazz);
    }

因为篇幅原因(咱不太会),我么忽略掉验证过程,所以verify()空空如也。

准备阶段主要是给类变量分配空间并给予初始值,prepare()方法马上再讲。

四、对象、实例变量和类变量

在第4章中,定义了LocalVars类,用来表示局部变量表。从逻辑上来看,LocalVars实例就像一个数组,这个数组的每一个元素都足够容纳一个int、float或引用值。要放入double或者long值,需要相邻的两个元素。这个结构体不是正好也可以用来表示类变量和实例变量吗?

所以我们创建一个Slots类,把LocalVars和Slot类中的代码拷贝进去,稍作修改,如下:

public class Slots {

    private Slot[] slots;

    public Slots(int slotCount) {
        if (slotCount > 0) {
            slots = new Slot[slotCount];
            for (int i = 0; i < slotCount; i++) {
                slots[i] = new Slot();
            }
        }
    }

    public void setInt(int idx, int val) {
        this.slots[idx].num = val;
    }

    public int getInt(int idx) {
        return this.slots[idx].num;
    }

    public void setFloat(int idx, float val) {
        this.slots[idx].num = (int) val;
    }

    public float getFloat(int idx) {
        return this.slots[idx].num;
    }

    public void setLong(int idx, long val) {
        this.slots[idx].num = (int) val;
        this.slots[idx + 1].num = (int) (val >> 32);
    }

    public long getLong(int idx) {
        int low = this.slots[idx].num;
        int high = this.slots[idx + 1].num;
        return (long) high << 32 | (long) low;
    }

    public void setDouble(int idx, double val) {
        this.setLong(idx, (long) val);
    }

    public Double getDouble(int idx) {
        return (double) this.getLong(idx);
    }

    public void setRef(int idx, Object ref) {
        this.slots[idx].ref = ref;
    }

    public Object getRef(int idx){
        return this.slots[idx].ref;
    }

}

Slots类准备就绪,可以使用了。Class类早在第一节就定义好了,代码如下:

public class Class {
    ...
        public Slots staticVars;
}        

Object类添加两个字段,一个存放对象的Class指针,一个存放实例变量,代码如下:

public class Object {

    Class clazz;
    Slots fields;
}    

接下来的两个问题是,如何知道静态变量和实例变量需要多少空间,以及哪个字段对应Slots中的哪个位置呢?

第一个问题比较好解决,只要数一下类的字段即可。假设某个类有m个静态字段和n个实例字段,那么静态变量和实例变量所需的空间大小就分别是m’和n’。这里要注意两点。首先,类是可以继承的。也就是说,在数实例变量时,要递归地数超类的实例变量;其次,long和double字段都占据两个位置,所以m’>=m,n’>=n。

第二个问题也不算难,在数字段时,给字段按顺序编上号就可以了。这里有三点需要要注意。首先,静态字段和实例字段要分开编号,否则会混乱。其次,对于实例字段,一定要从继承关系的最顶端,也就是java.lang.Object开始编号,否则也会混乱。最后,编号时也要考虑long和double类型。

我们给Filed类加上一个字段public int slotId;

再打开ClassLoader类`,在其中定义上prepare()方法,代码如下:

    private void prepare(Class clazz) {
        calcInstanceFieldSlotIds(clazz);
        calcStaticFieldSlotIds(clazz);
        allocAndInitStaticVars(clazz);
    }

calcInstanceFieldSlotIds()方法计算实例字段的个数,同时给它们编号,代码如下:

    private void calcInstanceFieldSlotIds(Class clazz) {
        int slotId = 0;
        if (clazz.superClass != null) {
            slotId = clazz.superClass.instanceSlotCount;
        }
        for (Field field : clazz.fields) {
            if (!field.isStatic()) {
                field.slotId = slotId;
                slotId++;
                if (field.isLongOrDouble()) {
                    slotId++;
                }
            }
        }
        clazz.instanceSlotCount = slotId;
    }

calcStaticFieldSlotIds()方法计算静态字段的个数,同时给它们编号,代码如下:

    private void calcStaticFieldSlotIds(Class clazz) {
        int slotId = 0;
        for (Field field : clazz.fields) {
            if (field.isStatic()) {
                field.slotId = slotId;
                slotId++;
                if (field.isLongOrDouble()) {
                    slotId++;
                }
            }
        }
        clazz.staticSlotCount = slotId;
    }

Field类的isLongOrDouble()方法返回字段是否是long或double类型,代码如下:

    public boolean isLongOrDouble() {
        return this.descriptor.equals("J") || this.descriptor.equals("D");
    }

allocAndInitStaticVars()方法给类变量分配空间,然后给它们赋予初始值,代码如下:

    private void allocAndInitStaticVars(Class clazz) {
        clazz.staticVars = new Slots(clazz.staticSlotCount);
        for (Field field : clazz.fields) {
            if (field.isStatic() && field.isFinal()) {
                initStaticFinalVar(clazz, field);
            }
        }
    }

如果静态变量属于基本类型或String类型,有final修饰符,且它的值在编译期已知,则该值存储在class文件常量池中。

initStaticFinalVar()方法从常量池中加载常量值,然后给静态变量赋值,代码如下:

   private void initStaticFinalVar(Class clazz, Field field) {
        Slots staticVars = clazz.staticVars;
        RunTimeConstantPool constantPool = clazz.runTimeConstantPool;
        int cpIdx = field.constValueIndex();
        int slotId = field.slotId();

        if (cpIdx > 0) {
            switch (field.descriptor()) {
                case "Z":
                case "B":
                case "C":
                case "S":
                case "I":
                    Object val = constantPool.getConstants(cpIdx);
                    staticVars.setInt(slotId, (Integer) val);
                case "J":
                    staticVars.setLong(slotId, (Long) constantPool.getConstants(cpIdx));
                case "F":
                    staticVars.setFloat(slotId, (Float) constantPool.getConstants(cpIdx));
                case "D":
                    staticVars.setDouble(slotId, (Double) constantPool.getConstants(cpIdx));
                case "Ljava/lang/String;":
                    System.out.println("todo");//字符串常量将在第八章讨论;
            }
        }

    }

需要给Field类添加constValueIndex字段public int constValueIndex。修改newFields()方法,从字段属性表中读取constValueIndex,代码改动如下:

   public Field[] newFields(Class clazz, MemberInfo[] cfFields) {
        Field[] fields = new Field[cfFields.length];
        for (int i = 0; i < cfFields.length; i++) {
            fields[i] = new Field();
            fields[i].clazz = clazz;
            fields[i].copyMemberInfo(cfFields[i]);
            fields[i].copyAttributes(cfFields[i]);
        }
        return fields;
    }

copyAttributes()方法的代码如下:

   public void copyAttributes(MemberInfo cfField) {
        ConstantValueAttribute valAttr = cfField.ConstantValueAttribute();
        if (null != valAttr) {
            this.constValueIndex = valAttr.constantValueIdx();
        }
    }

MemberInfo类中的ConstantValueAttribute()方法代码如下:

   public ConstantValueAttribute ConstantValueAttribute() {
        for (AttributeInfo attrInfo : attributes) {
            if (attrInfo instanceof ConstantValueAttribute) return (ConstantValueAttribute) attrInfo;
        }
        return null;
    }

五、 类和字段符号引用解析

本节讨论类符号引用和字段符号引用的解析,方法符号引用的解析将在第7章讨论。

1、类符号引用解析

打开Symref类,在其中定义ResolvedClass()方法,代码如下:

    public Class resolvedClass() {
        if (null != this.clazz) return this.clazz;
        Class d = this.runTimeConstantPool.getClazz();
        Class c = d.loader.loadClass(this.className);;
        if (!c.isAccessibleTo(d)) {
            System.out.println("java.lang.IllegalAccessError");
        }
        this.clazz = c;
        return this.clazz;
    }

如果类符号引用已经解析,则直接返回类指针。否则,如果类D通过符号引用N引用类C的话,要解析N,先用D的类加载器加载C,然后检查D是否有权限访问C,如果没有,则抛出IllegalAccessError异常。

Java虚拟机规范5.4.4节给出了类的访问控制规则,把这个规则翻译成Class类的isAccessibleTo()方法,代码如下:

public boolean isAccessibleTo(Class other) {
        return this.isPublic() || this.getPackageName().equals(other.getPackageName());
    }

也就是说,如果类D想访问类C,需要满足两个条件之一:C是public,或者C和D在同一个运行时包内。第11章再讨论运行时包,这里先简单按照包名来检查。

getPackageName()方法的代码如下(也在class类文件中):

    public String getPackageName() {
        int i = this.name.lastIndexOf("/");
        if (i >= 0) return this.name;
        return "";
    }

比如类名是java/lang/Object,则它的包名就是java/lang。如果类定义在默认包中,它的包名是空字符串。

2、字段符号引用解析

打开Fieldref类,在其中定义resolvedField()方法,代码如下:

  public Field resolvedField() {
        if (null == field) {
            try {
                this.resolveFieldRef();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
        return this.field;
    }

与上面大同小异。

Java虚拟机规范5.4.3.2节给出了字段符号引用的解析步骤,把它翻译成resolveFieldRef()方法,代码如下:

    private void resolveFieldRef() throws NoSuchFieldException {
        Class d = this.runTimeConstantPool.getClazz();
        Class c = this.resolvedClass();

        Field field = this.lookupField(c, this.name, this.descriptor);
        if (null == field){
            throw new NoSuchFieldException();
        }

        if (!field.isAccessibleTo(d)){
            throw new IllegalAccessError();
        }

        this.field = field;
    }

如果类D想通过字段符号引用访问类C的某个字段,首先要解析符号引用得到类C,然后根据字段名和描述符查找字段。如果字段查找失败,则虚拟机抛出NoSuchFieldError异常。如果查找成功,但D没有足够的权限访问该字段,则虚拟机抛出IllegalAccessError异常。字段查找步骤在lookupField()函数中,代码如下:

    private Field lookupField(Class c, String name, String descriptor) {
        for (Field field : c.fields) {
            if (field.name.equals(name) && field.descriptor.equals(descriptor)) {
                return field;
            }
        }

        for (Class iface : c.interfaces) {
            Field field = lookupField(iface, name, descriptor);
            if (null != field) return field;
        }

        if (c.superClass != null) {
            return lookupField(c.superClass, name, descriptor);
        }

        return null;
    }

首先在C的字段中查找。如果找不到,在C的直接接口递归应用这个查找过程。如果还找不到的话,在C的父类中递归应用这个查找过程。如果仍然找不到,则查找失败。

这个规则同样也适用于方法,所以把它(略做简化)实现成ClassMember类的isAccessibleTo()方法,代码如下(在ClassMember类文件中):

   public boolean isAccessibleTo(Class d) {
        if (this.isPublic()) {
            return true;
        }
        Class c = this.clazz;
        if (this.isProtected()) {
            return d == c || c.getPackageName().equals(d.getPackageName());
        }
        if (!this.isPrivate()) {
            return c.getPackageName().equals(d.getPackageName());
        }
        return d == c;
    }

}

用通俗的语言描述字段访问规则。如果字段是public,则任何类都可以访问。如果字段是protected,则只有子类和同一个包下的类可以访问。如果字段有默认访问权限(非public,非protected,也非privated),则只有同一个包下的类可以访问。否则,字段是private,只有声明这个字段的类才能访问

六、 类和对象相关指令

本节将实现10条类和对象相关的指令。new指令用来创建类实例;putstatic和getstatic指令用于存取静态变量;putfield和getfield用于存取实例变量;instanceof和checkcast指令用于判断对象是否属于某种类型;ldc系列指令把运行时常量池中的常量推到操作数栈顶。上面提到的指令除ldc以外,都属于引用类指令。下面的Java代码演示了这些指令的用处。

public class MyObject {
public static int staticVar;
public int instanceVar;
public static void main(String[] args) {
int x = 32768; // ldc
MyObject myObj = new MyObject(); // new
MyObject.staticVar = x; // putstatic
x = MyObject.staticVar; // getstatic
myObj.instanceVar = x; // putfield
x = myObj.instanceVar; // getfield
Object obj = myObj;
if (obj instanceof MyObject) { // instanceof
myObj = (MyObject) obj; // checkcast
}
}

这些指令在Instruction.references包下。references包与常量、加载、存储等指令包同级。

①new指令

注意,new指令专门用来创建类实例。数组由专门的指令创建,在第8章中实现数组和数组相关指令。new指令代码如下:

public class NEW extends InstructionIndex16 {

    @Override
    public void execute(Frame frame) {
        RunTimeConstantPool cp = frame.method().clazz().constantPool();
        ClassRef classRef = (ClassRef) cp.getConstants(this.idx);
        Class clazz = classRef.resolvedClass();
        if (clazz.isInterface() || clazz.isAbstract()) {
            throw new InstantiationError();
        }
        Object ref = clazz.newObject();
        frame.operandStack().pushRef(ref);
    }

}

new指令的操作数是一个uint16索引,来自字节码。通过这个索引,可以从当前类的运行时常量池中找到一个类符号引用。解析这个类符号引用,拿到类数据,然后创建对象,并把对象引用推入栈顶,new指令的工作就完成了。

因为接口和抽象类都不能实例化,所以如果解析后的类是接口或抽象类,按照Java虚拟机规范规定,需要抛出InstantiationError异常。另外,如果解析后的类还没有初始化,则需要先初始化类。在第7章实现方法调用之后会详细讨论类的初始化,这里暂时先忽略。Class结构体的NewObject()方法如下(在Class类文件中):

    public Object newObject() {
        return new Object(this);
    }

这里只是调用了Object类的Object()方法,代码如下(在Object类中):

    public Object(Class clazz){
        this.clazz = clazz;
        this.fields = new Slots(clazz.instanceSlotCount);
    }

new指令实现好了,下面看看如何存取类的静态变量。

②putstatic和getstatic指令

在references目录下创建PUT_STATIC类文件,在其中实现putstatic指令,代码如下:

public class PUT_STATIC extends InstructionIndex16 {
    @Override
    public void execute(Frame frame) {
        /*先拿到当前方法、当前类和当前常量池,然后解析字段符号引用。
        如果声明字段的类还没有被初始化,则需要先初始化该类,这部分逻辑将在第7章实现;
        */
        Method currentMethod = frame.method();
        Class currentClazz = currentMethod.clazz();
        RunTimeConstantPool runTimeConstantPool = currentClazz.constantPool();
        FieldRef fieldRef = (FieldRef) runTimeConstantPool.getConstants(this.idx);
        Field field = fieldRef.resolvedField();
        Class clazz = field.clazz();
        
        //根据字段类型从操作数栈中弹出相应的值,然后赋给静态变量;
        String descriptor = field.descriptor();
        int slotId = field.slotId();
        Slots slots = clazz.staticVars();
        OperandStack stack = frame.operandStack();
        switch (descriptor.substring(0, 1)) {
            case "Z":
            case "B":
            case "C":
            case "S":
            case "I":
                slots.setInt(slotId, stack.popInt());
                break;
            case "F":
                slots.setFloat(slotId, stack.popFloat());
                break;
            case "J":
                slots.setLong(slotId, stack.popLong());
                break;
            case "D":
                slots.setDouble(slotId, stack.popDouble());
                break;
            case "L":
            case "[":
                slots.setRef(slotId, stack.popRef());
                break;
            default:
                break;
        }
    }

}

至此,putstatic指令就解释完毕了。getstatic指令和putstatic正好相反,它取出类的某个静态变量值,然后推入栈顶。

在references目录下创建GET_STATIC类文件,在其中实现getstatic指令,代码如下:

public class GET_STATIC extends InstructionIndex16 {

    @Override
    public void execute(Frame frame) {

        /*用法和putstatic一样.
        如果解析后的字段不是静态字段,要抛出IncompatibleClassChangeError异常。
        */
        RunTimeConstantPool runTimeConstantPool = frame.method().clazz().constantPool();
        FieldRef ref = (FieldRef) runTimeConstantPool.getConstants(this.idx);
        Field field = ref.resolvedField();
        if (!field.isStatic()){
            throw new IncompatibleClassChangeError();
        }
        Class clazz = field.clazz();
        
        //根据字段类型,从静态变量中取出相应的值,然后推入操作数栈顶;
        String descriptor = field.descriptor();
        int slotId = field.slotId();
        Slots slots = clazz.staticVars();
        OperandStack stack = frame.operandStack();
        switch (descriptor.substring(0, 1)) {
            case "Z":
            case "B":
            case "C":
            case "S":
            case "I":
                stack.pushInt(slots.getInt(slotId));
                break;
            case "F":
                stack.pushFloat(slots.getFloat(slotId));
                break;
            case "J":
                stack.pushLong(slots.getLong(slotId));
                break;
            case "D":
                stack.pushDouble(slots.getDouble(slotId));
                break;
            case "L":
            case "[":
                stack.pushRef(slots.getRef(slotId));
                break;
            default:
                break;
        }
    }

}

至此,getstatic指令也解释完毕了。下面介绍如何存取对象的实例变量。

③putfield和getfield指令

在references目录下创建PUT_FIELD类文件,在其中实现putfield指令,代码如下:

public class PUT_FIELD extends InstructionIndex16 {

    @Override
    public void execute(Frame frame) {
        
        //基本上和putstatic一样;
        Method currentMethod = frame.method();
        Class currentClazz = currentMethod.clazz();
        RunTimeConstantPool runTimeConstantPool = currentClazz.constantPool();
        FieldRef fieldRef = (FieldRef) runTimeConstantPool.getConstants(this.idx);
        Field field = fieldRef.resolvedField();
        if (field.isStatic()) {

        }

        if (field.isFinal()) {

        }
        
        /*先根据字段类型从操作数栈中弹出相应的变量值,然后弹出对象引用。
        如果引用是null,需要抛出空指针异常(NullPointerException),
        否则通过引用给实例变量赋值;
        */
        String descriptor = field.descriptor();
        int slotId = field.slotId();
        OperandStack stack = frame.operandStack();
        switch (descriptor.substring(0, 1)) {
            case "Z":
            case "B":
            case "C":
            case "S":
            case "I":
                int valInt = stack.popInt();
                Object refInt = stack.popRef();
                if (null == refInt) {
                    throw new NullPointerException();
                }
                refInt.fields().setInt(slotId, valInt);
                break;
            case "F":
                float valFloat = stack.popFloat();
                Object refFloat = stack.popRef();
                if (null == refFloat) {
                    throw new NullPointerException();
                }
                refFloat.fields().setFloat(slotId, valFloat);
                break;
            case "J":
                long valLong = stack.popLong();
                Object refLong = stack.popRef();
                if (null == refLong) {
                    throw new NullPointerException();
                }
                refLong.fields().setLong(slotId, valLong);
                break;
            case "D":
                double valDouble = stack.popDouble();
                Object refDouble = stack.popRef();
                if (null == refDouble) {
                    throw new NullPointerException();
                }
                refDouble.fields().setDouble(slotId, valDouble);
                break;
            case "L":
            case "[":
                Object val = stack.popRef();
                Object ref = stack.popRef();
                if (null == ref) {
                    throw new NullPointerException();
                }
                ref.fields().setRef(slotId, val);
                break;
            default:
                break;
        }
    }

}

putfield指令解释完毕,下面来看getfield指令。代码如下:

public class GET_FIELD extends InstructionIndex16 {

    @Override
    public void execute(Frame frame) {
        RunTimeConstantPool runTimeConstantPool = frame.method().clazz().constantPool();
        FieldRef fieldRef = (FieldRef) runTimeConstantPool.getConstants(this.idx);
        Field field = fieldRef.resolvedField();
        
        //弹出对象引用,如果是null,则抛出NullPointerException;
        OperandStack stack = frame.operandStack();
        Object ref = stack.popRef();
        if (null == ref) {
            throw new NullPointerException();
        }
        
        //根据字段类型,获取相应的实例变量值,然后推入操作数栈;
        String descriptor = field.descriptor();
        int slotId = field.slotId();
        Slots slots = ref.fields();

        switch (descriptor.substring(0, 1)) {
            case "Z":
            case "B":
            case "C":
            case "S":
            case "I":
                stack.pushInt(slots.getInt(slotId));
                break;
            case "F":
                stack.pushFloat(slots.getFloat(slotId));
                break;
            case "J":
                stack.pushLong(slots.getLong(slotId));
                break;
            case "D":
                stack.pushDouble(slots.getDouble(slotId));
                break;
            case "L":
            case "[":
                stack.pushRef(slots.getRef(slotId));
                break;
            default:
                break;
        }
    }

}

④instanceof和checkcast指令

instanceof指令判断对象是否是某个类的实例(或者对象的类是否实现了某个接口),并把结果推入操作数栈。代码如下:

public class INSTANCE_OF extends InstructionIndex16 {

    @Override
    public void execute(Frame frame) {
        /*先弹出对象引用,如果是null,则把0推入操作数栈。
        如果对象引用不是null,则解析类符号引用,判断对象是否是类的实例,然后把判断结果推入操作数栈。  
        */
        OperandStack stack = frame.operandStack();
        Object ref = stack.popRef();
        if (null == ref){
            stack.pushInt(0);
            return;
        }
        RunTimeConstantPool cp = frame.method().clazz().constantPool();
        ClassRef classRef = (ClassRef) cp.getConstants(this.idx);
        Class clazz = classRef.resolvedClass();
        if (ref.isInstanceOf(clazz)){
            stack.pushInt(1);
        } else {
            stack.pushInt(0);
        }
    }

}

下面来看checkcast指令。代码如下:

public class CHECK_CAST extends InstructionIndex16 {

    @Override
    public void execute(Frame frame) {
        
        /*先从操作数栈中弹出对象引用,再推回去,这样就不会改变操作数栈的状态。
        如果引用是null,则指令执行结束。
        也就是说,null引用可以转换成任何类型,否则解析类符号引用,判断对象是否是类的实例。
        如果是的话,指令执行结束;
        */
        OperandStack stack = frame.operandStack();
        Object ref = stack.popRef();
        stack.pushRef(ref);
        if (null == ref) return;
        RunTimeConstantPool cp = frame.method().clazz().constantPool();
        ClassRef clazzRef = (ClassRef) cp.getConstants(this.idx);
        Class clazz = clazzRef.resolvedClass();
    }

}

instanceof和checkcast指令一般都是配合使用的,像下面的Java代码这样:

if (xxx instanceof ClassYYY) {
yyy = (ClassYYY) xxx;
// use yyy
}

然后是Object类的IsInstanceOf()方法的代码如下:

public boolean isInstanceOf(Class clazz){
        return clazz.isAssignableFrom(this.clazz);
    }

真正的逻辑在Class类的isAssignableFrom()方法中,代码如下:

//T代表other,S代表this;
 public boolean isAssignableFrom(Class other) {
        if (this == other) return true;
        if (!other.isInterface()) {
            return this.isSubClassOf(other);
        } else {
            return this.isImplements(other);
        }
    }

也就是说,在三种情况下,S类型的引用值可以赋值给T类型:S和T是同一类型;T是类且S是T的子类;或者T是接口且S实现了T接口。这是简化版的判断逻辑,因为还没有实现数组,第8章讨论数组时会继续完善这个方法。

在其中实现isSubClassOf()方法,判断S是否是T的子类。代码如下:

  public boolean isSubClassOf(Class other) {
        for (Class c = this.superClass; c != null; c = c.superClass) {
            if (c == other) {
                return true;
            }
        }
        return false;
    }

在其中实现isImplements()方法,判断S是否实现了T接口,就看S或S的(直接或间接)超类是否实现了某个接口T’,T’要么是T,要么是T的子接口。代码如下:

 private boolean isImplements(Class other) {

        for (Class c = this; c != null; c = c.superClass) {
            for (Class clazz : c.interfaces) {
                if (clazz == other || clazz.isSubInterfaceOf(other)) {
                    return true;
                }
            }
        }
        return false;

    }

在其中实现isSubInterfaceOf()方法,isSubInterfaceOf()方法和isSubClassOf()方法类似,但是用到了递归,代码如下:

   public boolean isSubInterfaceOf(Class iface) {
        for (Class superInterface : this.interfaces) {
            if (superInterface == iface || superInterface.isSubInterfaceOf(iface)) {
                return true;
            }
        }
        return false;
    }

}

⑤ldc指令

ldc系列指令从运行时常量池中加载常量值,并把它推入操作数栈。ldc系列指令属于常量类指令,共3条。其中ldc和ldc_w指令用于加载int、float和字符串常量,java.lang.Class实例或者MethodType和MethodHandle实例。ldc2_w指令用于加载long和double常量。ldc和ldc_w指令的区别仅在于操作数的宽度。

本章只处理int、float、long和double常量。第8章实现数组和字符串之后,会进一步完善ldc指令,支持字符串常量的加载。第9章还会继续完善ldc指令,支持Class实例的加载。到此,类和对象相关的10条指令都实现好了。最后还需要修instructions.factory类文件在其中添加这些指令的case语句。具体见代码,这里不打出来了。

ldc和ldc_w指令的逻辑完全一样,在LDC()函数中实现,先从当前类的运行时常量池中取出常量。如果是int或float常量,则提取出常量值,则推入操作数栈。代码如下:

public class LDC extends InstructionIndex8 {

    @Override
    public void execute(Frame frame) {
        _ldc(frame, this.idx);
    }

    private void _ldc(Frame frame, int idx) {
        OperandStack stack = frame.operandStack();
        RunTimeConstantPool runTimeConstantPool = frame.method().clazz().constantPool();
        java.lang.Object c = runTimeConstantPool.getConstants(idx);

        if (c instanceof Integer){
            stack.pushInt((Integer) c);
            return;
        }

        if (c instanceof Float){
            stack.pushFloat((Float) c);
            return;
        }

        throw new RuntimeException("todo ldc");
    }

}

ldc2_w指令的Execute()方法单独实现,代码如下:

public class LDC2_W extends InstructionIndex16 {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        RunTimeConstantPool runTimeConstantPool = frame.method().clazz().constantPool();
        Object c = runTimeConstantPool.getConstants(this.idx);
        if (c instanceof Long) {
            stack.pushLong((Long) c);
            return;
        }
        if (c instanceof Double){
            stack.pushDouble((Double) c);
        }
        throw new ClassFormatError();

    }

}

到此,类和对象相关的10条指令都实现好了。最后还需要修改instructions.factory类文件,在其中添加这些指令的case语句。具体见源码。

七、测试

main()函数不变,删掉其他函数,然后修改startJVM()函数,代码如下:

 private static void startJVM(Cmd cmd) {
        Classpath classpath = new Classpath(cmd.jre, cmd.classpath);
        ClassLoader classLoader = new ClassLoader(classpath);
        //获取className
        String className = cmd.getMainClass().replace(".", "/");
        Class mainClass = classLoader.loadClass(className);
        Method mainMethod = mainClass.getMainMethod();
        if (null == mainMethod) {
            throw new RuntimeException("Main method not found in class " + cmd.getMainClass());
        }
        new Interpret(mainMethod);
    }

先创建ClassLoader实例,然后用它来加载主类,最后执行主类的main()方法。Class类的GetMainMethod()方法如下:

 public Method getMainMethod() {
        return this.getStaticMethod("main", "([Ljava/lang/String;)V");
    }

它只是调用了getStaticMethod()方法而已,代码如下:

private Method getStaticMethod(String name, String descriptor) {
        for (Method method : this.methods) {
            if (method.name.equals(name) && method.descriptor.equals(descriptor)) {
                return method;
            }
        }
        return null;
    }

然后修改Interpret类中的interpret()函数,代码如下:

   Interpret(Method method) {
        Thread thread = new Thread();
        Frame frame = thread.newFrame(method);
        thread.pushFrame(frame);
        loop(thread, method.code());
    }

然后按照上章内容编译MyObject文件,输出结果如下:

"C:\Program Files\Java\jdk1.8.0_281\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2020.3.3\lib\idea_rt.jar=61527:C:\Program Files\JetBrains\IntelliJ IDEA 2020.3.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_281\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_281\jre\lib\rt.jar;D:\JavaProject\Idea-project\ZYX-demo-jvm\ZYX-demo-jvm-06\target\classes;D:\JAVA\maven\repository\com\beust\jcommander\1.72\jcommander-1.72.jar;D:\JAVA\maven\repository\org\projectlombok\lombok\1.18.0\lombok-1.18.0.jar;D:\JAVA\maven\repository\com\alibaba\fastjson\1.2.40\fastjson-1.2.40.jar" org.ZYX.demo.jvm.Main -Xjre "C:\Program Files\Java\jdk1.8.0_281\jre" D:\JavaProject\Idea-project\ZYX-demo-jvm\ZYX-demo-jvm-06\src\main\resources\MyObject
寄存器(指令)0x12 -> LDC => 局部变量表:[{"num":0},{"num":0},{"num":0},{"num":0}] 操作数栈:[{"num":0},{"num":0}]
寄存器(指令)0x3c -> ISTORE_1 => 局部变量表:[{"num":0},{"num":0},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"num":0}]
寄存器(指令)0xbb -> NEW => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"num":0}]
寄存器(指令)0x59 -> DUP => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768,"ref":{}},{"num":0}]
寄存器(指令)0xb7 -> INVOKE_SPECIAL => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768,"ref":{}},{"$ref":"$[0]"}]
寄存器(指令)0x4d -> ASTORE_2 => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
寄存器(指令)0x1b -> ILOAD_1 => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
寄存器(指令)0xb3 -> PUT_STATIC => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
寄存器(指令)0xb2 -> GET_STATIC => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
寄存器(指令)0x3c -> ISTORE_1 => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
寄存器(指令)0x2c -> ALOAD_2 => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
寄存器(指令)0x1b -> ILOAD_1 => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
寄存器(指令)0xb5 -> PUT_FIELD => 局部变量表:[{"num":0},{"num":32768},{"num":0},{"num":0}] 操作数栈:[{"num":32768},{"$ref":"$[0]"}]
Exception in thread "main" java.lang.NullPointerException
	at org.ZYX.demo.jvm.instructions.references.PUT_FIELD.execute(PUT_FIELD.java:42)
	at org.ZYX.demo.jvm.Interpret.loop(Interpret.java:42)
	at org.ZYX.demo.jvm.Interpret.<init>(Interpret.java:19)
	at org.ZYX.demo.jvm.Main.startJVM(Main.java:39)
	at org.ZYX.demo.jvm.Main.main(Main.java:26)

本章结束!

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值