文章目录
我们来总结一下。第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)
本章结束!