代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.对象、实例变量和类变量
在\rtda\heap目录下创建slots.go文件,把slot.go和local_vars.go文件中的内容拷贝进来,然后在此基础上修改,代码如下:
package heap
import "math"
type Slot struct {
num int32
ref *Object
}
type Slots []Slot
func newSlots(slotCount uint) Slots {
if slotCount > 0 {
return make([]Slot, slotCount)
}
return nil
}
func (self Slots) SetInt(index uint, val int32) {
self[index].num = val
}
func (self Slots) GetInt(index uint) int32 {
return self[index].num
}
func (self Slots) SetFloat(index uint, val float32) {
bits := math.Float32bits(val)
self[index].num = int32(bits)
}
func (self Slots) GetFloat(index uint) float32 {
bits := uint32(self[index].num)
return math.Float32frombits(bits)
}
// long consumes two slots
func (self Slots) SetLong(index uint, val int64) {
self[index].num = int32(val)
self[index+1].num = int32(val >> 32)
}
func (self Slots) GetLong(index uint) int64 {
low := uint32(self[index].num)
high := uint32(self[index+1].num)
return int64(high)<<32 | int64(low)
}
// double consumes two slots
func (self Slots) SetDouble(index uint, val float64) {
bits := math.Float64bits(val)
self.SetLong(index, int64(bits))
}
func (self Slots) GetDouble(index uint) float64 {
bits := uint64(self.GetLong(index))
return math.Float64frombits(bits)
}
func (self Slots) SetRef(index uint, ref *Object) {
self[index].ref = ref
}
func (self Slots) GetRef(index uint) *Object {
return self[index].ref
}Slots结构体准备就绪,可以使用了。Class结构体早在6.1节就定义好了,代码如下:

打开\rtda\heap\object.go文件,给Object结构体添加两个字段,一个存放对象的Class指针,一个存放实例变量,代码如下:
package heap
type Object struct {
class *Class
fields Slots
}
// getters
func (self *Object) Class() *Class {
return self.class
}
func (self *Object) Fields() Slots {
return self.fields
}
接下来有两个问题要处理,一是如何知道静态变量和实例变量需要多少空间,另外一个是哪个字段对应Slots中的哪个位置。
第一个问题比较好解决,只要数一下类的字段即可。假设某个类有m个静态字段和n个实例字段,那么静态变量和实例变量所需的空间大小就分别是m’和n’。这里要注意两点。首先,类是可以继承的。也就是说,在数实例变量时,要递归地数超类的实例变量;其次,long和double字段都占据两个位置,所以m’>=m,n’>=n。第二个问题也不算难,在数字段时,给字段按顺序编上号就可以了。这里有三点需要要注意。首先,静态字段和实例字段要分开编号,否则会混乱。其次,对于实例字段,一定要从继承关系的最顶端,也就是java.lang.Object开始编号,否则也会混乱。最后,编号时也要考虑long和double类型。
打开field.go文件,给Field结构体加上slotId字段,代码如下:
type Field struct {
ClassMember
slotId uint
}Field结构体的isLongOrDouble()方法返回字段是否是long或double类型,和加上Getter方法
func (self *Field) SlotId() uint {
return self.slotId
}
func (self *Field) isLongOrDouble() bool {
return self.descriptor == "J" || self.descriptor == "D"
}
在class_loader.go文件,在其中定义prepare()函数,代码如下:
// jvms 5.4.2
func prepare(class *Class) {
calcInstanceFieldSlotIds(class)
calcStaticFieldSlotIds(class)
allocAndInitStaticVars(class)
}
calcInstanceFieldSlotIds()函数计算实例字段的个数,同时给它们编号,代码如下:
func calcInstanceFieldSlotIds(class *Class) {
slotId := uint(0)
if class.superClass != nil {
slotId = class.superClass.instanceSlotCount
}
for _, field := range class.fields {
if !field.IsStatic() {
field.slotId = slotId
slotId++
if field.isLongOrDouble() {
slotId++
}
}
}
class.instanceSlotCount = slotId
}
calcStaticFieldSlotIds()函数计算静态字段的个数,同时给它们编号,代码如下:
func calcStaticFieldSlotIds(class *Class) {
slotId := uint(0)
for _, field := range class.fields {
if field.IsStatic() {
field.slotId = slotId
slotId++
if field.isLongOrDouble() {
slotId++
}
}
}
class.staticSlotCount = slotId
}
allocAndInitStaticVars()函数给类变量分配空间,然后给它们赋予初始值,代码如下:
func allocAndInitStaticVars(class *Class) {
class.staticVars = newSlots(class.staticSlotCount)
for _, field := range class.fields {
if field.IsStatic() && field.IsFinal() {
initStaticFinalVar(class, field)
}
}
}
因为Go语言会保证新创建的Slot结构体有默认值(num字段是0,ref字段是nil),而浮点数0编码之后和整数0相同,所以不用做任何操作就可以保证静态变量有默认初始值(数字类型是0,引用类型是null)。如果静态变量属于基本类型或String类型,有final修饰符,且它的值在编译期已知,则该值存储在class文件常量池中。
initStaticFinalVar()函数从常量池中加载常量值,然后给静态变量赋值,字符串常量将在第8章讨论,这里先调用panic()函数终止程序执行。代码如下:
func initStaticFinalVar(class *Class, field *Field) {
vars := class.staticVars
cp := class.constantPool
cpIndex := field.ConstValueIndex()
slotId := field.SlotId()
if cpIndex > 0 {
switch field.Descriptor() {
case "Z", "B", "C", "S", "I":
val := cp.GetConstant(cpIndex).(int32)
vars.SetInt(slotId, val)
case "J":
val := cp.GetConstant(cpIndex).(int64)
vars.SetLong(slotId, val)
case "F":
val := cp.GetConstant(cpIndex).(float32)
vars.SetFloat(slotId, val)
case "D":
val := cp.GetConstant(cpIndex).(float64)
vars.SetDouble(slotId, val)
case "Ljava/lang/String;":
panic("todo")
}
}
}
需要给Field结构体添加constValueIndex字段,修改newFields()方法,从字段属性表中读取constValueIndex,copyAttributes()方法的以及加上Getter方法代码如下:
type Field struct {
ClassMember
constValueIndex uint
slotId uint
}
func newFields(class *Class, cfFields []*classfile.MemberInfo) []*Field {
fields := make([]*Field, len(cfFields))
for i, cfField := range cfFields {
fields[i] = &Field{}
fields[i].class = class
fields[i].copyMemberInfo(cfField)
fields[i].copyAttributes(cfField)
}
return fields
}
func (self *Field) copyAttributes(cfField *classfile.MemberInfo) {
if valAttr := cfField.ConstantValueAttribute(); valAttr != nil {
self.constValueIndex = uint(valAttr.ConstantValueIndex())
}
}
func (self *Field) ConstValueIndex() uint {
return self.constValueIndex
}
MemberInfo结构体的ConstantValueIndex()方法在ch06\classfile\member_info.go文件中,代码如下:
func (self *MemberInfo) ConstantValueAttribute() *ConstantValueAttribute {
for _, attrInfo := range self.attributes {
switch attrInfo.(type) {
case *ConstantValueAttribute:
return attrInfo.(*ConstantValueAttribute)
}
}
return nil
}
2.类符号引用解析
打开cp_symref.go文件,在其中定义ResolvedClass()方法,代码如下:
func (self *SymRef) ResolvedClass() *Class {
if self.class == nil {
self.resolveClassRef()
}
return self.class
}
如果类符号引用已经解析,ResolvedClass()方法直接返回类指针,否则调用resolveClassRef()方法进行解析。
Java虚拟机规范5.4.3.1节给出了类符号引用的解析步骤,

resolveClassRef()方法就是按照这个步骤编写的(有一些简化),代码如下:
// jvms8 5.4.3.1
func (self *SymRef) resolveClassRef() {
// 获取当前Class指针
d := self.cp.class
// 通过需要引用的类的完全限定名加载类
c := d.loader.LoadClass(self.className)
// 判断d是否能有权限调用引用类c
if !c.isAccessibleTo(d) {
panic("java.lang.IllegalAccessError")
}
self.class = c
}
通俗地讲,如果类D通过符号引用N引用类C的话,要解析N,先用D的类加载器加载C,然后检查D是否有权限访问C,如果没有,则抛出IllegalAccessError异常。
Java虚拟机规范5.4.4节给出了类的访问控制规则,

把这个规则翻译成Class结构体的isAccessibleTo()方法,代码如下(在class.go文件中):
// jvms 5.4.4
func (self *Class) isAccessibleTo(other *Class) bool {
return self.IsPublic() ||
self.getPackageName() == other.getPackageName()
}
也就是说,如果类D想访问类C,需要满足两个条件之一:C是public,或者C和D在同一个运行时包内。第11章再讨论运行时包,这里先简单按照包名来检查。getPackageName()方法的代码如下(也在class.go文件中):
func (self *Class) getPackageName() string {
if i := strings.LastIndex(self.name, "/"); i >= 0 {
return self.name[:i]
}
return ""
}比如类名是java/lang/Object,则它的包名就是java/lang。如果类定义在默认包中,它的包名是空字符串。
3.字段符号引用解析
打开cp_fieldref.go文件,在其中定义ResolvedField()方法,代码如下:
func (self *FieldRef) ResolvedField() *Field {
if self.field == nil {
self.resolveFieldRef()
}
return self.field
}
Java虚拟机规范5.4.3.2节给出了字段符号引用的解析步骤,

把它翻译成resolveFieldRef()方法,如果类D想通过字段符号引用访问类C的某个字段,首先要解析符号引用得到类C,然后根据字段名和描述符查找字段。如果字段查找失败,则虚拟机抛出NoSuchFieldError异常。如果查找成功,但D没有足够的权限访问该字段,则虚拟机抛出IllegalAccessError异常。
// jvms 5.4.3.2
func (self *FieldRef) resolveFieldRef() {
d := self.cp.class
// 解析字段符号引用之前需要先解析字段所属的类
c := self.ResolvedClass()
//根据字段名和描述符查找字段
field := lookupField(c, self.name, self.descriptor)
if field == nil {
panic("java.lang.NoSuchFieldError")
}
if !field.isAccessibleTo(d) {
panic("java.lang.IllegalAccessError")
}
self.field = field
}
字段查找步骤在lookupField()函数中,代码如下,首先在C的字段中查找。如果找不到,在C的直接接口递归应用这个查找过程。如果还找不到的话,在C的超类中递归应用这个查找过程。如果仍然找不到,则查找失败。
// 根据字段名和描述符查找字段
func lookupField(c *Class, name, descriptor string) *Field {
for _, field := range c.fields {
if field.name == name && field.descriptor == descriptor {
return field
}
}
for _, iface := range c.interfaces {
if field := lookupField(iface, name, descriptor); field != nil {
return field
}
}
if c.superClass != nil {
return lookupField(c.superClass, name, descriptor)
}
return nil
}
Java虚拟机规范5.4.4节也给出了字段的访问控制规则。这个规则同样也适用于方法,

所以把它(略做简化)实现成ClassMember结构体的isAccessibleTo()方法,代码如下(在class_member.go文件中):
// jvms 5.4.4
func (self *ClassMember) isAccessibleTo(d *Class) bool {
if self.IsPublic() {
return true
}
// 判断d是否能有权限调用引用类c
c := self.class
// 如果self字段是protected,只有子类和同一包下的类可以访问(isSubClassOf()函数后面给出)
if self.IsProtected() {
return d == c || d.isSubClassOf(c) ||
c.getPackageName() == d.getPackageName()
}
// 如果是默认访问权限,则只有同一包下的类可以访问
if !self.IsPrivate() {
return c.getPackageName() == d.getPackageName()
}
// 如果是默认访问权限,则只有同一包下的类可以访问
return d == c
}
用通俗的语言描述字段访问规则。如果字段是public,则任何类都可以访问。如果字段是protected,则只有子类和同一个包下的类可以访问。如果字段有默认访问权限(非public,非protected,也非privated),则只有同一个包下的类可以访问。否则,字段是private,只有声明这个字段的类才能访问。
4.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


