文章目录
- 代码目录
- 一、字节码和指令集
- 二、指令和指令解码
- 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这个数字,这正是我们的计算结果!完工!