文章目录

  • 代码目录
  • 一、字节码和指令集
  • 二、指令和指令解码
  • Instruction
  • ①InstructionNoOperands
  • ②InstructionBranch
  • ③InstructionIndex8
  • ④InstructionIndex16
  • BytecodeReader
  • 三、九种指令的实现
  • 1、常量指令
  • ①nop指令:
  • ②const指令:
  • ③BIPUSH和SIPUSH指令:
  • 2、加载指令
  • 3、存储指令
  • 4、栈指令
  • ①pop和pop2指令:
  • ②dup指令:
  • ③swap指令:
  • 5、数字指令
  • ①算术指令:
  • ②位移指令:
  • ③布尔运算指令:
  • ④iinc指令:
  • 6、类型转换指令
  • 7、比较指令
  • ①lcmp指令:
  • ②fcmp和dcmp指令:
  • ③ifcond指令:
  • ④if_icmp指令:
  • ⑤if_acmp指令:
  • 8、控制指令
  • ①goto指令:
  • ②tableswitch指令:
  • ③lookupswitch指令:
  • 9、扩展指令
  • ①WIDE指令:
  • ②IFNULL和IFNONNULL指令:
  • ③GOTO_W指令:
  • 四、解释器
  • 五、测试



由第3章可知,编译之后的Java方法以字节码的形式存储在class文件中。在第4章中,初步实现了Java虚拟机栈、帧、操作数栈和局部变量表等运行时数据区。本章将在前两章的基础上编写一个简单的解释器,并且实现大约150条指令。

代码目录

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
    │             │   ├── 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

一、字节码和指令集

字节码就是运行在Java虚拟机上的机器码。我们已经知道,每一个类或者接口都会被Java编译器编译成一个class文件,类或接口的方法信息就放在class文件的method_info结构中。如果方法不是抽象的,也不是本地方法,方法的Java代码就会被编译器编译成字节码(即使方法是空的,编译器也会生成一条return语句),存放在method_info结构的Code属性中。

字节码中存放编码后的Java虚拟机指令。每条指令都以一个单字节的操作码(opcode)开头。由于只使用一字节表示操作码,显而易见,Java虚拟机最多只能支持256(2^8)条指令。

到第八版为止,Java虚拟机规范已经定义了205条指令,操作码分别是0(0x00)到202(0xCA)、254(0xFE)和255(0xFF)。为了方便记忆,每个操作码又有独自的助记符。

Java虚拟机规范把已经定义的205条指令按用途分成了11类,分别是:常量(constants)指令、加载(loads)指令、存储(stores)指令、操作数栈(stack)指令、数学(math)指令、转换(conversions)指令、比较(comparisons)指令、控制(control)指令、引用(references)指令、扩展(extended)指令和保留(reserved)指令。

本章将要实现的指令涉及11类中的9类。为了便于管理,我们把所有指令都共用的代码则放在base里。

二、指令和指令解码

Java虚拟机解释器的大致逻辑是一个循环,每次循环都包含三个部分:计算pc、指令解码、指令执行。我们采用一种方式:把指令抽象成接口,解码和执行逻辑写在具体的指令实现中。

Instruction

首先是定义Instruction指令接口,代码如下:

public interface Instruction {

    //从字节码中提取操作数;
    void fetchOperands(BytecodeReader reader);

    //执行指令逻辑;
    void execut (Frame frame);
    }

①InstructionNoOperands

再编写Instruction的实现类①InstructionNoOperands,表示没有操作数的指令,代码如下:

public class InstructionNoOperands implements Instruction {
    @Override
    public void fetchOperands(BytecodeReader reader) {
        //empty
    }

    @Override
    public void execute(Frame frame) {
        // nothing to do
    }
    }

②InstructionBranch

②InstructionBranch,表示跳转指令,代码如下:

public class InstructionBranch implements Instruction {

    //跳转偏移量;
    protected int offset;

    //从字节码中读取一个Int16整数,赋给Offset字段;
    @Override
    public void fetchOperands(BytecodeReader reader) {
        this.offset = reader.readShort();
    }

    @Override
    public void execute(Frame frame) {

    }
    }

③InstructionIndex8

③InstructionIndex8,存储和加载类指令需要根据索引存取局部变量表,索引由单字节操作数给出。把这类指令抽象成InstructionBranch结构体,用Index字段表示局部变量表索引。代码如下:

public class InstructionIndex8 implements Instruction {

    public int idx;

    //从字节码中读取一个int8整数,赋给Index字段;
    @Override
    public void fetchOperands(BytecodeReader reader) {
        this.idx = reader.readByte();
    }

    @Override
    public void execute(Frame frame) {

    }
    }

④InstructionIndex16

④InstructionIndex16,有一些指令需要访问运行时常量池,常量池索引由两字节操作数给出。把这类指令抽象InstructionIndex16结构体,用Index字段表示常量池索引。代码如下:

public class InstructionIndex16 implements Instruction {

    protected int idx;
    
    //从字节码中读取一个int16整数,赋给Index字段;
    @Override
    public void fetchOperands(BytecodeReader reader) {
        this.idx = reader.readShort();
    }

    @Override
    public void execute(Frame frame) {

    }
    
}

BytecodeReader

指令接口和“抽象”指令定义好了,再来看看BytecodeReader:

public class BytecodeReader {

    //code字段存放字节码,pc字段记录读取到了哪个字节;
    private byte[] codes;
    private int pc;

    //为了避免每次解码指令都新创建一个BytecodeReader实例,给它定义一个Reset()方法;
    public void reset(byte[] codes, int pc) {
        this.codes = codes;
        this.pc = pc;
    }

    public int pc() {
        return this.pc;
    }

    //[go]int8 = [java]byte
    public byte readByte() {
        byte code = this.codes[this.pc];
        this.pc++;
        return code;
    }

    //[go]int16 = [java]short,连续读取2字节;
    public short readShort() {
        byte byte1 = readByte();
        byte byte2 = readByte();
        return (short) ((byte1 << 8) | byte2);
    }

    //[go]int32 = [java]int,连续读取4字节;
    public int readInt() {
        int byte1 = this.readByte();
        int byte2 = this.readByte();
        int byte3 = this.readByte();
        int byte4 = this.readByte();
        return (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4;
    }

    //used by lookupswitch and tableswitch,后面实现这两种指令时再介绍;
    public int[] readInts(int n) {
        int[] ints = new int[n];
        for (int i = 0; i < n; i++) {
            ints[i] = this.readInt();
        }
        return ints;
    }

    //used by lookupswitch and tableswitcch;
    public void skipPadding() {
        while (this.pc % 4 != 0) {
            this.readByte();
        }
    }

}

三、九种指令的实现

在接下来的9个小节中,将按照分类依次实现约150条指令,占整个指令集的3/4。很多指令其实是非常相似的。比如iload、lload、fload、dload和aload这5条指令,除了操作的数据类型不同以外,代码几乎一样。再比如iload_0、iload_1、iload_2和iload_3这四条指令,只是iload指令的特例(局部变量表索引隐含在操作码中),操作逻辑完全一样。

1、常量指令

常量指令把常量推入操作数栈顶。常量可以来自三个地方:隐含在操作码里、操作数和运行时常量池。常量指令共有21条,本节实现其中的18条。另外3条是ldc系列指令,用于从运行时常量池中加载常量,将在第6章介绍。

①nop指令:

nop指令是最简单的一条指令,因为它什么也不用做。代码如下:

public class NOP extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        //啥也不用做
    }

}

②const指令:

这一系列指令把隐含在操作码中的常量值推入操作数栈顶。其中定义了15条指令,它们都继承自InstructionNoOperands:

ACONST_NULL 
DCONST_0 
DCONST_1 
FCONST_0
FCONST_1 
FCONST_2 
ICONST_M1 
ICONST_0
ICONST_1
ICONST_2
ICONST_3
ICONST_4
ICONST_5
LCONST_0
LCONST_1

以三条指令为例进行说明:
ACONST_NULL

//把null引用推入操作数栈顶;
public class ACONST_NULL extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        frame.operandStack().pushRef(null);
    }

}

DCONST_0

//把double型0推入操作数栈顶;
public class DCONST_0 extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        frame.operandStack().pushDouble(0.0);
    }

}

ICONST_M1

//把int型-1推入操作数栈顶
public class ICONST_M1 extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        frame.operandStack().pushInt(-1);
    }
    
}

③BIPUSH和SIPUSH指令:

bipush指令从操作数中获取一个byte型整数,扩展成int型,然后推入栈顶。sipush指令从操作数中获取一个short型整数,扩展成int型,然后推入栈顶。代码如下:

public class BIPUSH implements Instruction {

    private byte val;

    @Override
    public void fetchOperands(BytecodeReader reader) {
        this.val = reader.readByte();
    }

    @Override
    public void execute(Frame frame) {
        frame.operandStack().pushInt(val);
    }

}
public class SIPUSH implements Instruction {

    private short val;

    @Override
    public void fetchOperands(BytecodeReader reader) {
        this.val = reader.readShort();
    }

    @Override
    public void execute(Frame frame) {
         frame.operandStack().pushInt(val);
    }
}

2、加载指令

加载指令从局部变量表获取变量,然后推入操作数栈顶。加载指令共33条,按照所操作变量的类型可以分为6类:aload系列指令操作引用类型变量、dload系列操作double类型变量、fload系列操作float变量、iload系列操作int变量、lload系列操作long变量、xaload操作数组。本节实现其中的25条,数组和xaload系列指令将在第8章讨论。下面以iload系列为例介绍加载指令。

ILOAD extends InstructionIndex8;
ILOAD_0 extends InstructionNoOperands;
ILOAD_1 extends InstructionNoOperands;
ILOAD_2 extends InstructionNoOperands;
ILOAD_3 extends InstructionNoOperands;

下面是ILOAD的代码实现,iload指令的索引来自操作数:

public class ILOAD extends InstructionIndex8 {

    @Override
    public void execute(Frame frame) {
        int val = frame.localVars().getInt(this.idx);
        frame.operandStack().pushInt(val);
    }
}

然后是ILOAD_0等4条指令,其索引隐含在操作码中:

public class ILOAD_0 extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        int val = frame.localVars().getInt(0);
        frame.operandStack().pushInt(val);
    }
}

3、存储指令

和加载指令刚好相反,存储指令把变量从操作数栈顶弹出,然后存入局部变量表。和加载指令一样,存储指令也可以分为6类。以lstore系列指令为例进行介绍。

ASTORE extends InstructionIndex8;
ASTORE_0 extends InstructionNoOperands;
ASTORE_1 extends InstructionNoOperands;
ASTORE_2 extends InstructionNoOperands;
ASTORE_3 extends InstructionNoOperands;

下面是LSTORE的代码实现,lstore指令的索引来自操作数:

public class LSTORE extends InstructionIndex8 {

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

}

然后是LSTORE_2等4条指令,其索引隐含在操作码中:

public class LSTORE_2 extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        _lstore(frame, 2);
    }

}

4、栈指令

栈指令直接对操作数栈进行操作,共9条:pop和pop2指令将栈顶变量弹出,dup系列指令复制栈顶变量,swap指令交换栈顶的两个变量。

和其他类型的指令不同,栈指令并不关心变量类型。为了实现栈指令,需要给OperandStack结构体添加两个方法:pushSlot和popSlot,其实现如下:

public void pushSlot(Slot slot) {
        this.slots[this.size] = slot;
        this.size++;
    }

    public Slot popSlot(){
        this.size --;
        return this.slots[this.size];
    }

①pop和pop2指令:

pop指令把栈顶变量弹出,代码如下:

public class POP extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        stack.popSlot();
    }

}

pop指令只能用于弹出int、float等占用一个操作数栈位置的变量。double和long变量在操作数栈中占据两个位置,需要使用pop2指令弹出,代码如下:

public class POP2 extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        stack.popSlot();
        stack.popSlot();
    }

}

②dup指令:

其中定义了6条指令,它们都继承自InstructionNoOperands,如下:

DUP
DUP2
DUP2_X1
DUP2_X2
DUP_X1
DUP_X2

以两条指令为例进行说明:
DUP:复制栈顶的单个变量

public class DUP extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        Slot slot = stack.popSlot();
        stack.pushSlot(slot);
        stack.pushSlot(slot);
    }

}

DUP2则就是复制栈顶的两个变量。

DUP2_X1:复制顶部的一个或两个操作数堆栈值,向下插入两个或三个值。

public class DUP2_X1 extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        Slot slot1 = stack.popSlot();
        Slot slot2 = stack.popSlot();
        Slot slot3 = stack.popSlot();
         stack.pushSlot(slot2);
         stack.pushSlot(slot1);
         stack.pushSlot(slot3);
         stack.pushSlot(slot2);
         stack.pushSlot(slot1);
    }

}

DUP2_X2则就是复制顶部的一个或两个操作数堆栈值,向下插入两个、三个或四个值。

③swap指令:

SWAP指令交换栈顶的两个变量,代码如下:

public class SWAP extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        Slot slot1 = stack.popSlot();
        Slot slot2 = stack.popSlot();
        stack.pushSlot(slot1);
        stack.pushSlot(slot2);
    }
}

5、数字指令

数学指令大致对应Java语言中的加、减、乘、除等数学运算符。数学指令包括算术指令、位移指
令和布尔运算指令等,共37条,将全部在本节实现。

①算术指令:

算术指令又可以进一步分为加法(add)指令、减法(sub)指令、乘法(mul)指令、除法(div)指令、求余(rem)指令和取反(neg)指令6种。加、减、乘、除和取反指令都比较简单,我们以稍微复杂一些的求余指令为例进行讨论。
IREM和LREM
IREM和LREM差不多,以IREM为例,代码如下:

/*先从操作数栈中弹出两个int变量,求余,然后把结果推入操作数栈。
这里注意一点,对int或long变量做除法和求余运算时,是有可能抛出ArithmeticException异常的。
*/
public class IREM extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        int v2 = stack.popInt();
        int v1 = stack.popInt();
        if (v2 == 0) {
            throw new ArithmeticException("/ by zero");
        }
        int res = v1 % v2;
        stack.pushInt(res);
    }

}

DREM和FREM
DREM和FREM差不多,以DREM为例,代码如下:

public class DREM extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        double v2 = stack.popDouble();
        double v1 = stack.popDouble();
        double res = v1 % v2;
        stack.pushDouble(res);
    }

}

②位移指令:

位移指令可以分为左移和右移两种,右移指令又可以分为算术右移(有符号右移)和逻辑右移(无符号右移)两种。算术右移和逻辑位移的区别仅在于符号位的扩展(>>:带符号右移。正数右移高位补0,负数右移高位补1;>>>:无符号右移。无论是正数还是负数,高位通通补0。)。其中定义了6条位移指令,都继承自InstructionNoOperands:

ISHL     //int左位移
ISHR     //int算术右移
IUSHR    //int逻辑右移
LSHL     //long左位移
LSHR     //long算术右移
LUSHR    //long逻辑右移

左位移指令以ISHL为例:

/*先从操作数栈中弹出两个int变量v2和v1。
v1是要进行位移操作的变量,v2指出要移位多少比特。位移之后,把结果推入操作数栈。
这里注意,int变量只有32位,所以只取v2的前5个比特就足够表示位移位数了。
*/
public class ISHL extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        int v2 = stack.popInt();
        int v1 = stack.popInt();
        int s = v2 & 0x1f;
        int res = v1 << s;
        stack.pushInt(res);
    }

}

算术右移指令以LSHR为例:

/*算术右移指令需要扩展符号位,代码和左移指令基本上差不多。
long变量有64位,所以取v2的前6个比特。
*/
public class LSHR  extends InstructionNoOperands {
    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        int v2 = stack.popInt();
        long v1 = stack.popLong();
        long s = v2 & 0x3f;
        long res = v1 >> s;
        stack.pushLong(res);
    }

}

逻辑右移指令以IUSHR为例:

public class IUSHR extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        int v2 = stack.popInt();
        int v1 = stack.popInt();
        int s = v2 & 0x1f;
        int res = v1 >>> s;
        stack.pushInt(res);
    }

}

③布尔运算指令:

布尔运算指令只能操作int和long变量,分为按位与(and)、按位或(or)、按位异或(xor)3种。以按位与为例介绍布尔运算指令。
IAND和LAND,代码如下:

public class IAND extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        int v2 = stack.popInt();
        int v1 = stack.popInt();
        int res = v1 & v2;
        stack.pushInt(res);
    }
}
public class LAND extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        long v2 = stack.popLong();
        long v1 = stack.popLong();
        long res = v1 & v2;
        stack.pushLong(res);
    }

}

④iinc指令:

iinc指令给局部变量表中的int变量增加常量值,局部变量表索引和常量值都由指令的操作数提供,代码如下:

public class IINC implements Instruction {

    public int idx;
    public int constVal;
    
    //从字节码里读取操作数;
    @Override
    public void fetchOperands(BytecodeReader reader) {
        this.idx = reader.readByte();
        this.constVal = reader.readByte();
    }
    
    //从局部变量表中读取变量,给它加上常量值,再把结果写回局部变量表;   
    @Override
    public void execute(Frame frame) {
        LocalVars vars = frame.localVars();
        int val = vars.getInt(this.idx);
        val += this.constVal;
        vars.setInt(this.idx, val);
    }
}

6、类型转换指令

类型转换指令大致对应Java语言中的基本类型强制转换操作。类型转换指令有共15条,将全部在
本节实现。引用类型转换对应的是checkcast指令,将在第6章介绍。

按照被转换变量的类型,类型转换指令可以分为3种:i2x系列指令把int变量强制转换成其他类型;12x系列指令把long变量强制转换成其他类型;f2x系列指令把float变量强制转换成其他类型;d2x系列指令把double变量强制转换成其他类型。以d2x系列指令为例进行讨论。

id2x中可以定义d2f、d2i和d2l指令:

d2f extends InstructionNoOperands
d2i ……
d2l ……

以d2i为例,代码如下:

public class D2I extends InstructionNoOperands {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        double d = stack.popDouble();
        int i = (int) d;
        stack.pushInt(i);
    }

}

7、比较指令

比较指令可以分为两类:一类将比较结果推入操作数栈顶,一类根据比较结果跳转。比较指令是编译器实现if-else、for、while等语句的基石,共有19条,将全部在本节实现。

①lcmp指令:

lcmp指令用于比较long变量,代码如下:

public class LCMP extends InstructionNoOperands {
    
    //Execute()方法把栈顶的两个long变量弹出,进行比较,然后把比较结果(int型0、1或-1)推入栈顶;
    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        long v2 = stack.popLong();
        long v1 = stack.popLong();
        if (v1 > v2) {
            stack.pushInt(1);
            return;
        }
        if (v1 == v2) {
            stack.pushInt(0);
            return;
        }
        stack.pushInt(-1);
    }
}

②fcmp和dcmp指令:

fcmp:用于比较float变量。在fcmp中定义了fcmpg和fcmpl指令。这两条指令和lcmp指令很像,但是除了比较的变量类型不同以外,还有一个重要的区别。由于浮点数计算有可能产生NaN(Not a Number)值,所以比较两个浮点数时,除了大于、等于、小于之外,还有第4种结果:无法比较。fcmpg和fcmpl指令的区别就在于对第4种结果的定义。也就是说,当两个float变量中至少有一个是NaN时,用fcmpg指令比较的结果是1,而用fcmpl指
令比较的结果是-1。这里就不贴代码了。

dcmp:用于比较double变量,其余跟fcmp一样,这里就不详细介绍了。

③ifcond指令:

其中定义6条指令如下:

IFEQ extends InstructionBranch
IFGE ……
IFGT ……
IFLE ……
IFLT ……
IFNE ……

ifcond指令把操作数栈顶的int变量弹出,然后跟0进行比较,满足条件则跳转。假设从栈顶弹出的变量是x,则指令执行跳转操作的条件如下:
·ifeq:x==0
·ifne:x!=0
·iflt:x<0
·ifle:x<=0
·ifgt:x>0
·ifge:x>=0

以ifeq指令为例,代码如下:

public class IFEQ extends InstructionBranch {

    @Override
    public void execute(Frame frame) {
        int val = frame.operandStack().popInt();
        if (0 == val) {
            Instruction.branch(frame, this.offset);
        }
    }
}

真正的跳转逻辑在Branch()函数中。因为这个函数在很多指令中都会用到,所以把它定义在instruction.base.Instruction接口中,代码如下:

static void branch(Frame frame, int offset) {
        int pc = frame.thread().pc();
        int nextPC = pc + offset;
        frame.setNextPC(nextPC);
    }

Frame结构体的SetNextPC()方法将在五.四.小节解释器中介绍。

④if_icmp指令:

其中定义6条指令如下:

IF_ICMPEQ extends InstructionBranch
IF_ICMPGE……
IF_ICMPGT……
IF_ICMPLE……
IF_ICMPLT……
IF_ICMPNE……

if_icmp指令把栈顶的两个int变量弹出,然后进行比较,满足条件则跳转。跳转条件和if指令类似。以if_icmpne指令为例,代码如下:

public class IF_ICMPNE extends InstructionBranch {

    @Override
    public void execute(Frame frame) {
        OperandStack stack = frame.operandStack();
        int val2 = stack.popInt();
        int val1 = stack.popInt();
        if (val1 != val2) {
            Instruction.branch(frame, this.offset);
        }
    }

}

⑤if_acmp指令:

在其中定义两条if_acmp指令,if_acmpeq和if_acmpne,把栈顶的两个引用弹出,根据引用是否相同进行跳转。

IF_ACMPEQ extends InstructionBranch
IF_ACMPNE extends InstructionBranch

以if_acmpeq指令为例,代码如下:

public class IF_ACMPEQ extends InstructionBranch {

    @Override
    public void execute(Frame frame) {
        if (_acmp(frame)) {
            Instruction.branch(frame, this.offset);
        }
    }

}

8、控制指令

①goto指令:

goto指令进行无条件跳转,代码如下:

public class GOTO extends InstructionBranch {

    @Override
    public void execute(Frame frame) {
        Instruction.branch(frame, this.offset);
    }
}

②tableswitch指令:

Java语言中的switch-case语句有两种实现方式:如果case值可以编码成一个索引表,则实现成tableswitch指令;否则实现成lookupswitch指令。

下面这个Java方法中的switch-case可以编译成tableswitch指令,代码如下:

int chooseNear(int i) {
switch (i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}

下面这个Java方法中的switch-case则需要编译成lookupswitch指令:

int chooseFar(int i) {
switch (i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}
}

③lookupswitch指令:

9、扩展指令

扩展指令共有6条。和jsr指令一样,我们不讨论jsr_w指令。multianewarray指令用于创建多维数组,在第8章讨论数组时实现该指令。本节实现剩下的4条指令。

①WIDE指令:

加载类指令、存储类指令、ret指令和iinc指令需要按索引访问局部变量表,索引以uint8的形式存在字节码中。对于大部分方法来说,局部变量表大小都不会超过256,所以用一字节来表示索引就够了。但是如果有方法的局部变量表超过这限制呢?Java虚拟机规范定义了wide

wide指令改变其他指令的行为,modifiedInstruction字段存放被改变的指令。wide指令需要自己解码出modifiedInstruction,fetchOperands()方法的伪代码实现如下:

switch opcode {
case 0x15: ... // iload
case 0x16: ... // lload
case 0x17: ... // fload
case 0x18: ... // dload
case 0x19: ... // aload
case 0x36: ... // istore
case 0x37: ... // lstore
case 0x38: ... // fstore
case 0x39: ... // dstore
case 0x3a: ... // astore
case 0x84: ... // iinc
case 0xa9: // ret
     throw new RuntimeException("Unsupported opcode: 0xa9!")
}

②IFNULL和IFNONNULL指令:

根据引用是否是null进行跳转,wideifnull和ifnonnull指令把栈顶的引用弹出。以ifnull指令为例,代码如下:

public class IFNULL extends InstructionBranch {

    @Override
    public void execute(Frame frame) {
        Object ref = frame.operandStack().popRef();
        if (null == ref) {
            Instruction.branch(frame, this.offset);
        }
    }
}

③GOTO_W指令:

goto_w指令和goto指令的唯一区别就是索引从2字节变成了4字节。

public class GOTO_W implements Instruction {

    private int offset;

    @Override
    public void fetchOperands(BytecodeReader reader) {
        this.offset = reader.readInt();
    }

    @Override
    public void execute(Frame frame) {
        Instruction.branch(frame, this.offset);
    }

}

四、解释器

指令集已经实现得差不多了,本节编写一个简单的解释器。这个解释器目前只能执行一个Java方法,但是在后面的章节中,会不断完善它,让它变得越来越强大。

我们创建一个Interpret类,在其中定义interpret()函数,代码如下:

//interpret()方法的参数是MemberInfo指针;
    Interpret(MemberInfo m) {
        CodeAttribute codeAttr = m.codeAttribute(); //调用MemberInfo结构体的CodeAttribute()方法可以获取它的Code属性;
        
        //获得执行方法所需的局部变量表和操作数栈空间,以及方法的字节码;
        int maxLocals = codeAttr.maxLocals();
        int maxStack = codeAttr.maxStack();
        byte[] byteCode = codeAttr.data();
        
        //先创建一个Thread实例,然后创建一个帧并把它推入Java虚拟机栈顶,最后执行方法loop();
        Thread thread = new Thread();
        Frame frame = thread.newFrame(maxLocals, maxStack);
        thread.pushFrame(frame);
        loop(thread, byteCode);
    }

CodeAttribute()方法是新增的,代码写在classfile.MemberInfo接口中,代码如下:

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

Thread类的NewFrame()方法是新增加的,代码如下:

public Frame newFrame(int maxLocals, int maxStack) {
        return new Frame(this, maxLocals, maxStack);
    }

Frame类也有变化,增加了两个属性,这两个字段主要是为了实现跳转指令而添加的:

public class Frame {

    //stack is implemented as linked list
    Frame lower;

    //局部变量表
    private LocalVars localVars;

    //操作数栈
    private OperandStack operandStack;

    private Thread thread;

    private int nextPC;

这里我们回顾一下Instruction接口里的branch()方法:

static void branch(Frame frame, int offset) {
        int pc = frame.thread().pc();
        int nextPC = pc + offset;
        frame.setNextPC(nextPC);
    }

Frame类的Frame()方法也对应修改:

public Frame(Thread thread, int maxLocals, int maxStack) {
        this.thread = thread;
        this.localVars = new LocalVars(maxLocals);
        this.operandStack = new OperandStack(maxStack);
    }

再来看loop()方法:

private void loop(Thread thread, byte[] byteCode) {
        Frame frame = thread.popFrame();
        BytecodeReader reader = new BytecodeReader();

        while (true) {
            //循环
            int pc = frame.nextPC();
            thread.setPC(pc);
            //decode
            reader.reset(byteCode, pc);
            byte opcode = reader.readByte();
            Instruction inst = Factory.newInstruction(opcode);//newInstruction()方法在Factory类中;
            if (null == inst) {
                System.out.println("寄存器(指令)尚未实现 " + byteToHexString(new byte[]{opcode}));
                break;
            }
            inst.fetchOperands(reader);
            frame.setNextPC(reader.pc());
            //打印出局部变量表和操作数栈的内容;
            System.out.println("寄存器(指令):" + byteToHexString(new byte[]{opcode}) + " -> " + inst.getClass().getSimpleName() + " => 局部变量表:" + JSON.toJSONString(frame.operandStack().getSlots()) + " 操作数栈:" + JSON.toJSONString(frame.operandStack().getSlots()));            //exec
            inst.execute(frame);
        }

    }

最后是我们的Factory类,它根据操作码创建具体的指令,是一大堆switch-case语句。代码过长,只贴一小段,具体的可以进我的源码去看:

public static Instruction newInstruction(byte opcode) {
        switch (opcode) {
            case 0x00:
                return new NOP();
            case 0x01:
                return new ACONST_NULL();
            case 0x02:
                return new ICONST_M1();
            case 0x03:
                return new ICONST_0();
            ......

五、测试

我们用德国大数学家高斯的一个广为流传的小故事。有一天,数学老师布置了一道题:问1+2+3……这样从1一直加到100等于多少。小高斯很快就给出了答案。高斯不是从1一直加到100,而是用更聪明的办法计算的:1+100=101,2+99=101……1加到100有50组这样的数,所以50*101=5050。

我们将测试代码里的HelloWorld更改为:

public class HelloWorld {
    
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
    
}

main类中修改startJVM()函数,改动如下:

private static void startJVM(Cmd cmd) {
        Classpath classpath = new Classpath(cmd.jre, cmd.classpath);
        System.out.printf("classpath:%s class:%s args:%s\n", classpath, cmd.getMainClass(), cmd.getAppArgs());
        String className = cmd.getMainClass().replace(".", "/");
        ClassFile classFile = loadClass(className, classpath);
        MemberInfo mainMethod = getMainMethod(classFile);
        if (null == mainMethod) {
            System.out.println("Main method not found in class " + cmd.classpath);
            return;
        }
        new Interpret(mainMethod);
    }

startJVM()首先调用loadClass()方法读取并解析class文件,然后调用getMainMethod()函数查找类的main()方法,最后调用interpret()函数解释执行main()方法。loadClass()函数的代码如下:

private static ClassFile loadClass(String className, Classpath cp) {
        try {
            byte[] classData = cp.readClass(className);
            return new ClassFile(classData);
        } catch (Exception e) {
            System.out.println("Could not find or load main class " + className);
            e.printStackTrace();
        }
        return null;
    }

getMainMethod()函数的代码如下:

//找到主函数入口;
 private static MemberInfo getMainMethod(ClassFile cf) {
        if (null == cf) return null;
        MemberInfo[] methods = cf.methods();
        for (MemberInfo m : methods) {
            if ("main".equals(m.name()) && "([Ljava/lang/String;)V".equals(m.descriptor())) {
                return m;
            }
        }
        return null;

然后我们编译,将它通过 javac 命令编译成 class 文件,再把class文件放置到resources下。

启动参数也需要对应修改为,-Xjre “C:\Program Files\Java\jdk1.8.0_281\jre” “D:\JavaProject\Idea-project\ZYX-demo-jvm\ZYX-demo-jvm-05\src\main\resources\HelloWorld”。

输出结果如下,太长了贴一小段:

classpath:org.ZYX.demo.jvm.classpath.Classpath@7591083d class:D:\JavaProject\Idea-project\ZYX-demo-jvm\ZYX-demo-jvm-05\src\main\resources\HelloWorld args:null
寄存器(指令):0x03 -> ICONST_0 => 局部变量表:[{"num":0},{"num":0}] 操作数栈:[{"num":0},{"num":0}]
寄存器(指令):0x3c -> ISTORE_1 => 局部变量表:[{"num":0},{"num":0}] 操作数栈:[{"num":0},{"num":0}]
寄存器(指令):0x04 -> ICONST_1 => 局部变量表:[{"num":0},{"num":0}] 操作数栈:[{"num":0},{"num":0}]
......
寄存器(指令):0x84 -> IINC => 局部变量表:[{"num":5050},{"num":100}] 操作数栈:[{"num":5050},{"num":100}]
寄存器(指令):0xa7 -> GOTO => 局部变量表:[{"num":5050},{"num":100}] 操作数栈:[{"num":5050},{"num":100}]
寄存器(指令):0x1c -> ILOAD_2 => 局部变量表:[{"num":5050},{"num":100}] 操作数栈:[{"num":5050},{"num":100}]
寄存器(指令):0x10 -> BIPUSH => 局部变量表:[{"num":101},{"num":100}] 操作数栈:[{"num":101},{"num":100}]
寄存器(指令):0xa3 -> IF_ICMPGT => 局部变量表:[{"num":101},{"num":100}] 操作数栈:[{"num":101},{"num":100}]
寄存器(指令)尚未实现 0xb2

仔细观察局部变量表可以看到5050这个数字,这正是我们的计算结果!完工!