我们已经知道,要想运行Java程序,除了Java虚拟机之外,还需要Java类库的配合。Java虚拟机和Java类库的配合。Java虚拟机和Java类库一起构成了Java运行环境。Java类库主要用Java语言编写,一些无法用Java语言实现的方法则使用本地语言编写,这些方法叫做本地方法。从本章开始将陆续实现一些Java类中的本地方法。为了不陷入JNI规范的细节之中,我们将使用Go语言来实现。本章的所有代码位于/native包下。
注册和查找本地方法
在开始实现本地方法之前,先实现一个本地方法注册表,用来注册和查找本地方法。新建/native/registry.go文件:
本地方法定义成一个函数,参数是Frame结构体,没有返回值。这个frame参数就是本地方法的工作空间,也就是连接Java虚拟机和Java类库的桥梁,后面会看到它如何发挥作用的。registry是一个哈希表,值是具体的本地方法实现,至于键是什么,先看下Register()函数:
类名、方法名和方法描述符加在一起才能唯一确定一个方法,所以把它们的组合作为本地方法注册表的键。继续编辑registry.go文件,在其中实现FindNativeMethod():
之前说过,java.lang.Object等类是通过有一个叫做registerNatives()的本地方法来注册其他本地方法的,这里我们自己注册所有的本地方法实现,所以像registerNatives()这样的方法就没有太大用处了,为了避免重复代码,这里统一处理,如果遇到这样的本地方法,就返回一个空的实现。
调用本地方法
之前是用一段hack代码来跳过本地方法的执行的,现在可以来完善这段代码了。先删除/instructions/base/method_invoke_logic.go文件的InvokeMethod()方法中的hack代码删除。Java虚拟机规范并没有规定如何实现和调用本地方法,这给了我们充分的空间来发挥自己的想象力。我们将利用Java虚拟机栈来执行本地方法,所以除了删除hack代码之外,不用做任何修改。但是本地方法并没有字节码,如何利用Java虚拟机栈来执行呢?Java虚拟机规范预留了两条指令,操作码分别是0xFE和0xFF。下面将使用0xFE指令来达到目的,修改/rtdata/heap/method.go文件的newMethod()方法,下面贴出主要修改的地方:
本地方法在class文件中没有Code属性,所以需要给maxStack和maxLocals字段赋值。本地方法帧的操作数栈至少要能容纳返回值,为了简化代码,暂时给maxStack字段赋值为4。因为本地方法帧的局部变量表只用来存放参数值,所以把argSlotCount赋给maxLocals字段正好。至于code字段,也就是本地方法的字节码,第一条指令都是0xFE,第二条指令则根据函数的返回值选择相应的返回指令。
下面就来实现0xFE指令,实现代码位于/instructions/reserved/invokenative.go文件中,后面就把0xFE成为invokenative指令,代码如下:
顺便再把/instructions/factory.go文件中,对应的invokenative指令case注释放开。到此,准备工作就做好了,下面只要实现本地方法了。
反射
Java的反射机制非常强大,本节只涉及冰山一脚。
类和对象之间的关系
在Java中,类叶表现为普通的对象,它的类是java.lang.Class。听起来有点像鸡生蛋还是蛋生鸡的问题:类也是对象,而对象又是类的实例。看到这里,有点蒙了,为了帮助理解类和类对象之间的关系,我们想象一个简化的Java虚拟机运行时状态:方法区中只加载了两个类,java.lang.Object和java.lang.Class;堆中只通过new指令分配了一个对象。此时Java虚拟机的内存状态如下图所示:
图中区分了堆和方法区,方法区中的class_Object和class_Class分别是java.lang.Object和java.lang.Class的类数据,也就是对应我们Go项目的Class结构体;堆中的object_Object和object_Class分别是java.lang.Object和java.lang.Class的类对象,是java.lang.Class的实例,该功能下面会实现。object_XXX是单独的java.lang.Object实例。类对象我们暂且这样理解,下面继续聊反射。
Java有强大的反射能力,可以在运行期间获取类的各种信息、存取静态和实例变量、调用方法,等等。要想运用这种能力,获取类对象是第一步。在Java语言中,有两种方式可以获得类对象引用:使用类字面值和调用对象的getClass()方法:
之前通过Object结构体的class字段建立了类和对象之间的单向关系,现在把这个关系补充完整,让它成为双向的。修改/rtdata/heap/class.go文件:
通过jClass字段,每个Class结构体实例都与一个类对象关联。再修改/rtdata/heap/object.go文件:
extra字段用来记录Object结构体实例的额外信息。
下节继续将反射。