代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.类和对象之间的关系
在Java中,类也表现为普通的对象,它的类是java.lang.Class。类也是对象,而对象又是类的实例。
Java有强大的反射能力。可以在运行期间获取类的各种信息、存取静态和实例变量、调用方法,等等。要想运用这种能力,获取类对象是第一步。
在Java语言中,有两种方式可以获得类对象引用:使用类字面值和调用对象的getClass()方法。下面的Java代码演示了这两种方式。
System.out.println(String.class);
System.out.println("abc".getClass());
前面通过Object结构体的class字段建立了类和对象之间的单向关系。现在把这个关系补充完整,让它成为双向的。
打开\rtda\heap\class.go文件,修改Class结构体,添加jClass字段并定义Getter方法,改动如下:
通过jClass字段,每个Class结构体实例都与一个类对象关联。
type Class struct {
accessFlags uint16
name string // thisClassName
superClassName string
interfaceNames []string
constantPool *ConstantPool
fields []*Field
methods []*Method
loader *ClassLoader
superClass *Class
interfaces []*Class
instanceSlotCount uint
staticSlotCount uint
staticVars Slots
initStarted bool
jClass *Object
}对应Getter方法
func (self *Class) JClass() *Object {
return self.jClass
}
下面打开\rtda\heap\object.go文件,修改Object结构体,添加extra字段,这个字段之所以是interface{}类型,是因为它还有其他用途。目前只用它来记录类对象对应的Class结构体指针。改动如下:
type Object struct {
class *Class
data interface{} // Slots for Object, []int32 for int[] ...
extra interface{}
}extra字段用来记录Object结构体实例的额外信息。同样给它定义Getter和Setter方法
func (self *Object) Extra() interface{} {
return self.extra
}
//setter
func (self *Object) SetExtra(extra interface{}) {
self.extra = extra
}
由于/rtdata/heap/object.go文件添加了extra属性,所以在array_class.go和string_pool.go文件中初始化Object结构体的时候要多加个参数,如图:


假设方法区中只加载了两个类,java.lang.Object和java.lang.Class;堆中只通过new指令分配了一个对象。此时Java虚拟机的内存状态如图所示:

图只画出了Class和Object结构体的必要字段,并且刻意分开了堆和方法区。在方法区中,class1和class2分别是java.lang.Object和java.lang.Class类的数据。在堆中,object1和object2分别是
java.lang.Object和java.lang.Class的类对象。object3是单独的java.lang.Object实例。
再看一张图;

2.修改类加载器
Class和Object结构体准备好了,接下来修改类加载器,让每一个加载到方法区中的类都有一个类对象与之相关联。
打开\rtda\heap\class_loader.go文件,修改NewClassLoader()函数,改动如下:
func NewClassLoader(cp *classpath.Classpath, verboseFlag bool) *ClassLoader {
loader := &ClassLoader{
cp: cp,
verboseFlag: verboseFlag,
classMap: make(map[string]*Class),
}
// 加载java.lang.Class类
loader.loadBasicClasses()
return loader
}在返回ClassLoader结构体实例之前,先调用loadBasicClasses()函数。在class_loader.go文件中,加入loadBasicClasses()函数,loadBasicClasses()函数先加载java.lang.Class类,这又会触发java.lang.Object等类和接口的加载。然后遍历classMap,给已经加载的每一个类关联类对象。代码如下:
func (self *ClassLoader) loadBasicClasses() {
// 加载java.lang.Class类,会触发java.lang.Object等类和接口的加载
jlClassClass := self.LoadClass("java/lang/Class")
// 遍历已经加载的每一个类
for _, class := range self.classMap {
// 给每个类设置关联类对象
if class.jClass == nil {
class.jClass = jlClassClass.NewObject()
class.jClass.extra = class
}
}
}
下面修改LoadClass()方法,在类加载完之后,看java.lang.Class是否已经加载。如果是,则给类关联类对象。代码如下:
func (self *ClassLoader) LoadClass(name string) *Class {
if class, ok := self.classMap[name]; ok {
// already loaded
return class
}
var class *Class
// 若类名的第一个字符是'[',表示该类是数组类
if name[0] == '[' {
// array class
class = self.loadArrayClass(name)
} else {
class = self.loadNonArrayClass(name)
}
// 如果已加载java.lang.Class类,则给类关联类对象
if jlClassClass, ok := self.classMap["java/lang/Class"]; ok {
class.jClass = jlClassClass.NewObject()
class.jClass.extra = class
}
return class
}这样,在loadBasicClasses()和LoadClass()方法的配合之下,所有加载到方法区的类都设置好了jClass字段。
3.基本类型的类
void和基本类型也有对应的类对象,但只能通过字面值来访问,如下面的Java代码所示。
举几个例子:
System.out.println(void.class); System.out.println(boolean.class); System.out.println(byte.class);
和数组类一样,基本类型的类也是由Java虚拟机在运行期间生成的。继续编辑class_loader.go文件,修改NewClassLoader()函数,增加loadPrimitiveClasses()方法加载void和基本类型的类,代码如下:
func NewClassLoader(cp *classpath.Classpath, verboseFlag bool) *ClassLoader {
loader := &ClassLoader{
cp: cp,
verboseFlag: verboseFlag,
classMap: make(map[string]*Class),
}
// 加载java.lang.Class类
loader.loadBasicClasses()
//加载void和基本类型的类
loader.loadPrimitiveClasses()
return loader
}
loadPrimitiveClasses()方法加载void和基本类型的类,生成void和基本类型类的代码在loadPrimitiveClass()方法中,代码如下:
func (self *ClassLoader) loadPrimitiveClasses() {
for primitiveType, _ := range primitiveTypes {
self.loadPrimitiveClass(primitiveType)
}
}
func (self *ClassLoader) loadPrimitiveClass(className string) {
class := &Class{
accessFlags: ACC_PUBLIC, // todo
name: className,
loader: self,
initStarted: true,
}
class.jClass = self.classMap["java/lang/Class"].NewObject()
class.jClass.extra = class
self.classMap[className] = class
}
这里有三点需要说明:
- 第一,void和基本类型的类名就是void、int、float等。
- 第二,基本类型的类没有超类,也没有实现任何接口。
- 第三,非基本类型的类对象是通过ldc指令加载到操作数栈中的,基本类型的类对象,虽然在Java代码中看起来是通过字面量获取的,但是编译之后的指令并不是ldc,而是getstatic。每个基本类型都有一个包装类,包装类中有一个静态常量,叫作TYPE,其中存放的就是基本类型的类。也就是说,基本类型的类是通过getstatic指令访问相应包装类的TYPE字段加载到操作数栈中。
例如java.lang.Integer类。

4.修改ldc指令
和基本类型、字符串字面值一样,类对象字面值也是由ldc指令加载的。本节修改ldc指令,让它可以加载类对象。
打开\instructions\constants\ldc.go文件,修改_ldc()函数,增加了一个case语句,如果运行时,常量池中的常量是类引用,则解析类引用,然后把类的类对象推入操作数栈顶。改动如下:
// 支持类对象
case *heap.ClassRef:
classRef := c.(*heap.ClassRef)
classObj := classRef.ResolvedClass().JClass()
stack.PushRef(classObj)
5.通过反射获取类名
为了支持通过反射获取类名,本小节将实现以下4个本地方法:
Object.getClass()返回对象的类对象引用。
java.lang.Object.getClass()
基本类型的包装类在初始化时会调用Class.getPrimitiveClass()给TPYE字段赋值。
java.lang.Class.getPrimitiveClass()
Character类是基本类型char的包装类,它在初始化时会调用Class.desiredAssertionStatus0()方法。
java.lang.Class.desiredAssertionStatus0()
要实现getName0()方法,是因为Class.getName()方法是依赖这个本地方法工作的,
java.lang.Class.getName0()
getName()方法的代码如下:

在\native\java\lang\Object.go文件,在其中注册getClass()本地方法,首先,从局部变量表中拿到this引用。有了this引用后,通过Class()方法拿到它的Class结构体指针,进而又通过JClass()方法拿到它的类对象。最后,把类对象推入操作数栈顶。代码如下:
package lang
import (
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
)
const jlObject = "java/lang/Object"
func init() {
native.Register(jlObject, "getClass", "()Ljava/lang/Class;", getClass)
}
// public final native Class<?> getClass();
// ()Ljava/lang/Class;
func getClass(frame *rtda.Frame) {
// 从局部变量表拿到this引用
this := frame.LocalVars().GetThis()
// 获取类对象
class := this.Class().JClass()
// 将类对象入栈
frame.OperandStack().PushRef(class)
}
GetThis()方法在/rtda/local_vars.go文件中
func (self LocalVars) GetThis() *heap.Object {
return self.GetRef(0)
}在ch09/rtda/heap/class.go加上JavaName()方法,只是返回转换后的类名而已,下面需要使用。
func (self *Class) JavaName() string {
return strings.Replace(self.name, "/", ".", -1)
}
在/rtda/operand_stack.go,加入将布尔值推入和弹出操作数栈顶的方法,下面需要使用。
func (self *OperandStack) PushBoolean(val bool) {
if val {
self.PushInt(1)
} else {
self.PushInt(0)
}
}
func (self *OperandStack) PopBoolean() bool {
return self.PopInt() == 1
}
在\native\java\lang目录下创建Class.go文件,在其中注册3个本地方法,代码如下:
package lang
import (
"jvmgo/ch09/native"
"jvmgo/ch09/rtda"
"jvmgo/ch09/rtda/heap"
)
const jlClass = "java/lang/Class"
func init() {
//注册3个本地方法
native.Register(jlClass, "getPrimitiveClass", "(Ljava/lang/String;)Ljava/lang/Class;", getPrimitiveClass)
native.Register(jlClass, "getName0", "()Ljava/lang/String;", getName0)
native.Register(jlClass, "desiredAssertionStatus0", "(Ljava/lang/Class;)Z", desiredAssertionStatus0)
}
// static native Class<?> getPrimitiveClass(String name);
// (Ljava/lang/String;)Ljava/lang/Class;
func getPrimitiveClass(frame *rtda.Frame) {
//先从局部变量表中拿到类名
nameObj := frame.LocalVars().GetRef(0)
//这是个Java字符串,需要把它转成Go字符串
name := heap.GoString(nameObj)
//调用类加载器的LoadClass()方法获取
loader := frame.Method().Class().Loader()
class := loader.LoadClass(name).JClass()
//把类对象引用推入操作数栈顶
frame.OperandStack().PushRef(class)
}
// private native String getName0();
// ()Ljava/lang/String;
func getName0(frame *rtda.Frame) {
//从局部变量表中拿到this引用
this := frame.LocalVars().GetThis()
//通过Extra()方法可以获得与之对应的Class结构体指针
class := this.Extra().(*heap.Class)
//拿到类名
name := class.JavaName()
//转成Java字符串
nameObj := heap.JString(class.Loader(), name)
//推入操作数栈顶
frame.OperandStack().PushRef(nameObj)
}
// private static native boolean desiredAssertionStatus0(Class<?> clazz);
// (Ljava/lang/Class;)Z
func desiredAssertionStatus0(frame *rtda.Frame) {
// todo
//把false推入操作数栈顶
frame.OperandStack().PushBoolean(false)
}
解释一下上面代码:
getPrimitiveClass()是静态方法。先从局部变量表中拿到类名,这是个Java字符串,需要把它转成Go字符串。基本类型的类已经加载到了方法区中,直接调用类加载器的LoadClass()方法获取即可。最后,把类对象引用推入操作数栈顶。

下面实现getName0()方法,首先从局部变量表中拿到this引用,这是一个类对象引用,通过Extra()方法可以获得与之对应的Class结构体指针。然后拿到类名,转成Java字符串并推入操作数栈顶。注意这里需要的是java.lang.Object这样的类名,而非java/lang/Object。Class结构体的JavaName()方法返回转换后的类名

desiredAssertionStatus0()方法把false推入操作数栈顶

编辑\instructions\reserved\invokenative.go文件,在其中导入lang包,如果没有任何包依赖lang包,它就不会被编译进可执行文件,上面的本地方法也就不会被注册。所以需要一个地方导入lang包,把它放在invokenative.go文件中。由于没有显示使用lang中的变量或函数,所以必须在包名前面加上下划线,否则无法通过编译。
import (
"jvmgo/ch09/instructions/base"
"jvmgo/ch09/native"
_ "jvmgo/ch09/native/java/lang"
"jvmgo/ch09/rtda"
)
这个技术在Go语言中叫作“import for side effect”

测试代码
用Java代码测试
package jvmgo.book.ch06;
public class GetClassTest {
public static void main(String[] args) {
System.out.println(void.class.getName()); // void
System.out.println(boolean.class.getName()); // boolean
System.out.println(byte.class.getName()); // byte
System.out.println(char.class.getName()); // char
System.out.println(short.class.getName()); // short
System.out.println(int.class.getName()); // int
System.out.println(long.class.getName()); // long
System.out.println(float.class.getName()); // float
System.out.println(double.class.getName()); // double
System.out.println(Object.class.getName()); // java.lang.Object
System.out.println(int[].class.getName()); // [I
System.out.println(int[][].class.getName()); // [[I
System.out.println(Object[].class.getName()); // [Ljava.lang.Object;
System.out.println(Object[][].class.getName()); // [[Ljava.lang.Object;
System.out.println(Runnable.class.getName()); // java.lang.Runnable
System.out.println("abc".getClass().getName()); // java.lang.String
System.out.println(new double[0].getClass().getName()); // [D
System.out.println(new String[0].getClass().getName()); //[Ljava.lang.String;
}
}打开命令行窗口,执行下面的命令编译本章代码:
go install jvmgo\ch09
ch09 -classpath D:\MAT_log -Xjre "D:\software\java\jre" GetClassTest

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


