代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
new指令用来创建类实例;
putstatic和getstatic指令用于存取静态变量;
putfield和getfield用于存取实例变量;
instanceof和checkcast指令用于判断对象是否属于某种类型;
ldc系列指令把运行时常量池中的常量推到操作数栈顶。
Java代码演示。
public void newInstance() {
Object obj = new Object();
File file = new File("yutian.txt");
}
上面提到的指令除ldc以外,都属于引用类指令,
在ch06\instructions目录下创建references子目录来存放引用类指令。首先实现new指令。
1.new指令
创建类实例的指令:new
它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
在\instructions\references目录下创建new.go文件,在其中实现new指令,代码如下:
package references
import (
"jvmgo/ch06/instructions/base"
"jvmgo/ch06/rtda"
"jvmgo/ch06/rtda/heap"
)
// Create new object
type NEW struct{ base.Index16Instruction }
func (self *NEW) Execute(frame *rtda.Frame) {
// 获取运行时常量池
cp := frame.Method().Class().ConstantPool()
// 通过索引从常量池中获取类符号引用
classRef := cp.GetConstant(self.Index).(*heap.ClassRef)
class := classRef.ResolvedClass()
// todo: init class
if class.IsInterface() || class.IsAbstract() {
panic("java.lang.InstantiationError")
}
// 创建对象引用
ref := class.NewObject()
// 创建对象引用
frame.OperandStack().PushRef(ref)
}
new指令的操作数是一个uint16索引,来自字节码。通过这个索引,可以从当前类的运行时常量池中找到一个类符号引用。解析这个类符号引用,拿到类数据,然后创建对象,并把对象引用推入栈顶,new指令的工作就完成了。因为接口和抽象类都不能实例化,所以如果解析后的类是接口或抽象类,按照Java虚拟机规范规定,需要抛出InstantiationError异常。另外,如果解析后的类还没有初始化,则需要先初始化类。在第7章实现方法调用之后会详细讨论类的初始化,这里暂时先忽略。

Class结构体的NewObject()方法如下(在class.go文件中):
func (self *Class) NewObject() *Object {
return newObject(self)
}这里只是调用了Object结构体的newObject()方法,代码如下(在object.go中):
func newObject(class *Class) *Object {
return &Object{
class: class,
fields: newSlots(class.instanceSlotCount),
}
}新创建对象的实例变量都应该赋好初始值,但GO已经自动赋值。new指令实现好了,下面看看如何存取类的静态变量。
2.putstatic和getstatic指令
字段访问指令
对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。
- 访问类字段(static字段,或者称为类变量)的指令: getstatic、 putstatic
- 访问类实例字段(非static字段,或者称为实例变量)的指令: getfield, putfield
举例:以getstatic指令为例,它含有一个操作数,为指向常量池的Fieldref索引,它的作用就是获取Fieldref指定的对象或者值,并将其压入操作数栈。
如以下代码:
public void setOrderId(){
Order order = new Order();
order.id = 1001;
System.out.println(order.id);
Order.name = "ORDER";
System.out.println(Order.name);
}class Order{
int id;
static String name;
}
putstatic指令
putstatic指令给类的某个静态变量赋值,它需要两个操作数。第一个操作数是uint16索引,来自字节码。通过这个索引可以从当前类的运行时常量池中找到一个字段符号引用,解析这个符号引用
就可以知道要给类的哪个静态变量赋值。第二个操作数是要赋给静态变量的值,从操作数栈中弹出。
在references目录下创建putstatic.go文件,在其中实现putstatic指令,代码如下:
package references
import (
"jvmgo/ch06/instructions/base"
"jvmgo/ch06/rtda"
"jvmgo/ch06/rtda/heap"
)
// Set static field in class
type PUT_STATIC struct{ base.Index16Instruction }
func (self *PUT_STATIC) Execute(frame *rtda.Frame) {
// 获取当前正在执行的方法
currentMethod := frame.Method()
// 获取当前类
currentClass := currentMethod.Class()
// 获取运行时常量池
cp := currentClass.ConstantPool()
// 获取运行时常量池
fieldRef := cp.GetConstant(self.Index).(*heap.FieldRef)
field := fieldRef.ResolvedField()
class := field.Class()
// todo: init class
if !field.IsStatic() {
panic("java.lang.IncompatibleClassChangeError")
}
if field.IsFinal() {
// 如果字段是final,则表示静态常量,只能在类初始化方法中赋值
if currentClass != class || currentMethod.Name() != "<clinit>" {
panic("java.lang.IllegalAccessError")
}
}
// 获取字段描述符,也就是字段的类型
descriptor := field.Descriptor()
// 获取字段在Slots中的索引
slotId := field.SlotId()
// 获取字段在Slots中的索引
slots := class.StaticVars()
// 获取操作数栈
stack := frame.OperandStack()
// 通过字段的类型从操作数栈顶弹出相应的值,并给Class的静态变量Slots赋值
switch descriptor[0] {
case 'Z', 'B', 'C', 'S', 'I':
slots.SetInt(slotId, stack.PopInt())
case 'F':
slots.SetFloat(slotId, stack.PopFloat())
case 'J':
slots.SetLong(slotId, stack.PopLong())
case 'D':
slots.SetDouble(slotId, stack.PopDouble())
case 'L', '[':
slots.SetRef(slotId, stack.PopRef())
default:
// todo
}
}
Execute()方法稍微有些复杂,分三部分介绍:
1.先拿到当前方法、当前类和当前常量池,然后解析字段符号引用。如果声明字段的类还没有被初始化,则需要先初始化该类,这部分逻辑将在第7章实现。继续看代码:

2.如果解析后的字段是实例字段而非静态字段,则抛出IncompatibleClassChangeError异常。如果是final字段,则实际操作的是静态常量,只能在类初始化方法中给它赋值。否则,会抛出IllegalAccessError异常。类初始化方法由编译器生成,名字是<clinit>,具体请看第7章。继续看代码:

3.根据字段类型从操作数栈中弹出相应的值,然后赋给静态变量。

getstatic指令
getstatic指令和putstatic正好相反,它取出类的某个静态变量值,然后推入栈顶。在references目录下创建getstatic.go文件,在其中实现getstatic指令,代码如下:
getstatic指令只需要一个操作数:uint16常量池索引,用法和putstatic一样,代码如下:
package references
import (
"jvmgo/ch06/instructions/base"
"jvmgo/ch06/rtda"
"jvmgo/ch06/rtda/heap"
)
// Get static field from class
type GET_STATIC struct{ base.Index16Instruction }
func (self *GET_STATIC) Execute(frame *rtda.Frame) {
cp := frame.Method().Class().ConstantPool()
fieldRef := cp.GetConstant(self.Index).(*heap.FieldRef)
field := fieldRef.ResolvedField()
class := field.Class()
// todo: init class
if !field.IsStatic() {
panic("java.lang.IncompatibleClassChangeError")
}
descriptor := field.Descriptor()
slotId := field.SlotId()
slots := class.StaticVars()
stack := frame.OperandStack()
switch descriptor[0] {
case 'Z', 'B', 'C', 'S', 'I':
stack.PushInt(slots.GetInt(slotId))
case 'F':
stack.PushFloat(slots.GetFloat(slotId))
case 'J':
stack.PushLong(slots.GetLong(slotId))
case 'D':
stack.PushDouble(slots.GetDouble(slotId))
case 'L', '[':
stack.PushRef(slots.GetRef(slotId))
default:
// todo
}
}如果解析后的字段不是静态字段,也要抛出IncompatibleClassChangeError异常。如果声明字段的类还没有初始化好,也需要先初始化。

getstatic只是读取静态变量的值,不用管它是否是final了。根据字段类型,从静态变量中取出相应的值,然后推入操作数栈顶。

3.putfield和getfield指令
访问类实例字段(非static字段,或者称为实例变量)的指令: getfield, putfield
在references目录下创建putfield.go文件,在其中实现putfield指令,代码如下:
package references
import (
"jvmgo/ch06/instructions/base"
"jvmgo/ch06/rtda"
"jvmgo/ch06/rtda/heap"
)
// Set field in object
type PUT_FIELD struct{ base.Index16Instruction }
func (self *PUT_FIELD) Execute(frame *rtda.Frame) {
// 获取当前正在执行的方法
currentMethod := frame.Method()
// 获取当前类
currentClass := currentMethod.Class()
// 获取运行时常量池
cp := currentClass.ConstantPool()
// 通过索引从常量池中获取字段符号引用
fieldRef := cp.GetConstant(self.Index).(*heap.FieldRef)
field := fieldRef.ResolvedField()
if field.IsStatic() {
panic("java.lang.IncompatibleClassChangeError")
}
if field.IsFinal() {
// 如果字段是final,则表示静态常量,只能在类初始化方法中赋值
if currentClass != field.Class() || currentMethod.Name() != "<init>" {
panic("java.lang.IllegalAccessError")
}
}
// 获取字段描述符,也就是字段的类型
descriptor := field.Descriptor()
// 获取字段描述符,也就是字段的类型
slotId := field.SlotId()
// 获取操作数栈
stack := frame.OperandStack()
switch descriptor[0] {
case 'Z', 'B', 'C', 'S', 'I':
val := stack.PopInt()
ref := stack.PopRef()
if ref == nil {
panic("java.lang.NullPointerException")
}
ref.Fields().SetInt(slotId, val)
case 'F':
val := stack.PopFloat()
ref := stack.PopRef()
if ref == nil {
panic("java.lang.NullPointerException")
}
ref.Fields().SetFloat(slotId, val)
case 'J':
val := stack.PopLong()
ref := stack.PopRef()
if ref == nil {
panic("java.lang.NullPointerException")
}
ref.Fields().SetLong(slotId, val)
case 'D':
val := stack.PopDouble()
ref := stack.PopRef()
if ref == nil {
panic("java.lang.NullPointerException")
}
ref.Fields().SetDouble(slotId, val)
case 'L', '[':
val := stack.PopRef()
ref := stack.PopRef()
if ref == nil {
panic("java.lang.NullPointerException")
}
ref.Fields().SetRef(slotId, val)
default:
// todo
}
}
putfield指令给实例变量赋值,它需要三个操作数。前两个操作数是常量池索引和变量值,用法和putstatic一样。第三个操作数是对象引用,从操作数栈中弹出。

第一,解析后的字段必须是实例字段,否则抛出IncompatibleClassChangeError。第二,如果是final字段,则只能在构造函数中初始化,否则抛出IllegalAccessError。在第7章会介绍构造函数。

先根据字段类型从操作数栈中弹出相应的变量值,然后弹出对象引用。如果引用是null,需要抛出著名的空指针异常(NullPointerException),否则通过引用给实例变量赋值。

在references目录下创建getfield.go文件,在其中实现getfield指令,代码如下:
package references
import (
"jvmgo/ch06/instructions/base"
"jvmgo/ch06/rtda"
"jvmgo/ch06/rtda/heap"
)
// Fetch field from object
type GET_FIELD struct{ base.Index16Instruction }
func (self *GET_FIELD) Execute(frame *rtda.Frame) {
cp := frame.Method().Class().ConstantPool()
fieldRef := cp.GetConstant(self.Index).(*heap.FieldRef)
field := fieldRef.ResolvedField()
if field.IsStatic() {
panic("java.lang.IncompatibleClassChangeError")
}
stack := frame.OperandStack()
ref := stack.PopRef()
if ref == nil {
panic("java.lang.NullPointerException")
}
descriptor := field.Descriptor()
slotId := field.SlotId()
slots := ref.Fields()
switch descriptor[0] {
case 'Z', 'B', 'C', 'S', 'I':
stack.PushInt(slots.GetInt(slotId))
case 'F':
stack.PushFloat(slots.GetFloat(slotId))
case 'J':
stack.PushLong(slots.GetLong(slotId))
case 'D':
stack.PushDouble(slots.GetDouble(slotId))
case 'L', '[':
stack.PushRef(slots.GetRef(slotId))
default:
// todo
}
}getfield指令获取对象的实例变量值,然后推入操作数栈,它需要两个操作数。第一个操作数是uint16索引,用法和前面三个指令一样。第二个操作数是对象引用,用法和putfield一样。代码简单,不在阐述。
4.instanceof和checkcast
类型检查指令
检查类实例或数组类型的指令: instanceof、checkcast.
- 指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常。
- 指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈。
instanceof指令判断对象是否是某个类的实例(或者对象的类是否实现了某个接口),并把结果推入操作数栈。
在references目录下创建instanceof.go文件,在其中实现instanceof指令,代码如下:
package references
import (
"jvmgo/ch06/instructions/base"
"jvmgo/ch06/rtda"
"jvmgo/ch06/rtda/heap"
)
// Determine if object is of given type
type INSTANCE_OF struct{ base.Index16Instruction }
func (self *INSTANCE_OF) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
ref := stack.PopRef()
if ref == nil {
stack.PushInt(0)
return
}
cp := frame.Method().Class().ConstantPool()
classRef := cp.GetConstant(self.Index).(*heap.ClassRef)
class := classRef.ResolvedClass()
if ref.IsInstanceOf(class) {
stack.PushInt(1)
} else {
stack.PushInt(0)
}
}在Java中,如果引用obj是null的话,不管XXX是哪种类型,下面这条if判断都是false:
if (obj instanceof XXX) {
...
}
而instanceof指令需要两个操作数。第一个操作数是uint16索引,从方法的字节码中获取,通过这个索引可以从当前类的运行时常量池中找到一个类符号引用。第二个操作数是对象引用,从操作数栈中弹出。
instanceof指令的Execute(),先弹出对象引用,如果是null,则把0推入操作数栈。如果对象引用不是null,则解析类符号引用,判断对象是否是类的实例,然后把判断结果推入操作数栈。

判断对象是否是类的实例,在object结构体的IsInstanceOf()方法加入代码(在object.go文件中):
func (self *Object) IsInstanceOf(class *Class) bool {
return class.isAssignableFrom(self.class)
}
而又调用Class结构体的isAssignableFrom()方法,在ch06\rtda\heap目录下创建class_hierarchy.go文件,在其中定义isAssignableFrom()方法,在三种情况下,S类型的引用值可以赋值给T类型:S和T是同一类型;T是类且S是T的子类;或者T是接口且S实现了T接口。因为还没有实现数组,第8章讨论数组时会继续完善这个方法。
代码如下:
// jvms8 6.5.instanceof
// jvms8 6.5.checkcast
func (self *Class) isAssignableFrom(other *Class) bool {
s, t := other, self
if s == t {
return true
}
if !t.IsInterface() {
return s.isSubClassOf(t)
} else {
return s.isImplements(t)
}
}
继续编辑class_hierarchy.go文件,在其中实现isSubClassOf()方法,判断S是否是T的子类,就是判断T是否是S的(直接或间接)超类。循环赋值c为self的父类,看看相不相等,代码如下:
// self extends c
func (self *Class) isSubClassOf(other *Class) bool {
for c := self.superClass; c != nil; c = c.superClass {
if c == other {
return true
}
}
return false
}
继续编辑class_hierarchy.go文件,判断S是否实现了T接口,就看S或S的(直接或间接)超类是否实现了某个接口T’,T’要么是T,要么是T的子接口。 在其中实现isImplements()方法,就是每次循环赋值c为self的父类时,再循环c的接口,看看相不相等或者是否满足isSubInterfaceOf()方法,代码如下:
// self implements iface
func (self *Class) isImplements(iface *Class) bool {
for c := self; c != nil; c = c.superClass {
for _, i := range c.interfaces {
if i == iface || i.isSubInterfaceOf(iface) {
return true
}
}
}
return false
}
isSubInterfaceOf()方法也在class_hierarchy.go文件中,递归self的interfaces赋给superInterface,判断是否是T的子接口,代码如下:
// self extends iface
func (self *Class) isSubInterfaceOf(iface *Class) bool {
for _, superInterface := range self.interfaces {
if superInterface == iface || superInterface.isSubInterfaceOf(iface) {
return true
}
}
return false
}
下面来看checkcast指令。在references目录下创建checkcast.go文件,在其中实现checkcast指令,代码如下:
package references
import (
"jvmgo/ch06/instructions/base"
"jvmgo/ch06/rtda"
"jvmgo/ch06/rtda/heap"
)
// Check whether object is of given type
type CHECK_CAST struct{ base.Index16Instruction }
func (self *CHECK_CAST) Execute(frame *rtda.Frame) {
stack := frame.OperandStack()
ref := stack.PopRef()
stack.PushRef(ref)
if ref == nil {
return
}
cp := frame.Method().Class().ConstantPool()
classRef := cp.GetConstant(self.Index).(*heap.ClassRef)
class := classRef.ResolvedClass()
if !ref.IsInstanceOf(class) {
panic("java.lang.ClassCastException")
}
}instanceof指令会改变操作数栈(弹出对象引用,推入判断结果);checkcast则不改变操作数栈(如果判断失败,直接抛出ClassCastException异常)。先从操作数栈中弹出对象引用,再推回去,这样就不会改变操作数栈的状态。如果引用是null,则指令执行结束。也就是说,null引用可以转换成任何类型,否则解析类符号引用,判断对象是否是类的实例。如果是的话,指令执行结束,否则抛出ClassCastException。IsInstanceOf()方法上面说了。

5.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


