柯儿的天空头像
关注
【DIY系列:Java虚拟机】第59篇:反射实现——Class.forName 的 Go 版封面图

【DIY系列:Java虚拟机】第59篇:反射实现——Class.forName 的 Go 版

上一篇【第58篇】本地方法调用总览——打通 Java 与 C 的边界
下一篇【第60篇】基础类型包装类与自动装箱——Integer/Long 的秘密


摘要

第 058 篇搭好了本地方法的机制(注册表 + 0xFE 指令),这一篇开始填充内容——实现第一批本地方法:反射。

核心是一个哲学问题:类也是对象,那到底是先有类还是先有对象?

java.lang.Class 是一个类
  → 它有实例(类对象)
  → 每个类对象描述一个类
  → 其中一个类就是 java.lang.Class 本身  ← 💀 循环了!

书里把这个称为"鸡生蛋还是蛋生鸡"的问题,然后用两个字段破解:

  • Class.jClass *Object —— 类 → 它的类对象
  • Object.extra interface{} —— 类对象 → 它描述的类

有了双向指针,实现 getClass() 只要 3 行代码:

func getClass(frame *rtda.Frame) {
    this := frame.LocalVars().GetThis()
    class := this.Class().JClass()      // 对象的类 → 该类的类对象
    frame.OperandStack().PushRef(class)
}

外加 getPrimitiveClass()、getName0()、desiredAssertionStatus0(),以及 loadBasicClasses() / loadPrimitiveClasses() 两个"自举"函数。

一、类与对象的双向关系

1.1 原来的单向关系

第 6 章定义的 Object:

type Object struct {
    class  *Class      // 对象 → 它的类(单向)
    fields Slots
}

这个方向是自然的:每个对象都知道自己的类。

但反射需要反方向:拿到一个类对象(java.lang.Class 的实例),要能知道它描述的是哪个类。

Class<?> c = "abc".getClass();     // c 是个对象
c.getName();                        // 要返回 "java.lang.String"
                                    // 也就是:从对象 c 反查出它描述的 Class

1.2 加上两个字段

// ch09/rtda/heap/class.go
type Class struct {
    accessFlags       uint16
    name              string
    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      // 【新】java.lang.Class 实例
}

// ch09/rtda/heap/object.go
type Object struct {
    class  *Class
    data   interface{}
    extra  interface{}              // 【新】额外信息
}

Class.jClass —— 类 → 类对象的指针。

Object.extra —— 类对象 → 类的反向指针。

        Class (Go 结构体)                    Object (java.lang.Class 实例)
        ┌──────────────────┐                ┌──────────────────┐
        │ name: "java/lang/String"          │ class: ──────────┼──▶ Class( java/lang/Class )
        │ methods: [...]   │                │ data:  Slots{}   │
        │ fields:  [...]   │                │ extra: ──────────┼──┐
        │ jClass: ─────────┼────┐           └──────────────────┘  │
        └──────────────────┘    │                    ▲            │
                                └────────────────────┘            │
                                        双向关联 ◀─────────────────┘

为什么 extra 的类型是 interface{} 而不是 *Class?

因为 extra 是个通用扩展槽,后面还会用来装别的东西(第 10 章会用它存栈帧信息)。如果定死成 *Class,就失去扩展性了。

代价和 data interface{} 一样:取值时要类型断言 this.Extra().(*heap.Class)。

1.3 为什么不用"给 Class 加个字段"直接存名字

有人可能会想:类对象的 data(Slots)里不是有实例变量吗?把类名存进去不就行了?

public final class Class<T> {
    private String name;         // ← 能不能直接存这里?
    private ClassLoader classLoader;
    // ...
}

不行,因为:

  1. Class 类的实例变量有很多(name、classLoader、componentType、cachedConstructor 等),我们只实现一个子集
  2. 有些字段是 JDK 版本相关的(JDK 8 vs JDK 9 结构不同)
  3. 最关键的:从 Object 反查 *Class(Go 结构体)必须是 O(1) 的直接指针,存字符串还得再查一次 map

所以用 extra 存 Go 结构体指针,是最直接高效的方案。

二、破"鸡生蛋":loadBasicClasses

2.1 问题的本质

① 要创建 java/lang/Class 的类对象,需要先加载 java/lang/Class 类
② 加载 java/lang/Class 类时,按规则要给它关联一个类对象
③ 但类对象的类就是 java/lang/Class,它还在加载中 → 循环!

2.2 解法:两阶段 + 后置补齐

书里的方案很巧妙,分两步:

第一步:loadBasicClasses() 在 ClassLoader 构造时调用

func NewClassLoader(cp *classpath.Classpath, verboseFlag bool) *ClassLoader {
    loader := &ClassLoader{
        cp:          cp,
        verboseFlag: verboseFlag,
        classMap:    make(map[string]*Class),
    }
    loader.loadBasicClasses()
    loader.loadPrimitiveClasses()
    return loader
}

func (self *ClassLoader) loadBasicClasses() {
    jlClassClass := self.LoadClass("java/lang/Class")     // ← 先加载 Class 类
    for _, class := range self.classMap {                  // ← 再回头补齐
        if class.jClass == nil {
            class.jClass = jlClassClass.NewObject()
            class.jClass.extra = class
        }
    }
}

关键点:LoadClass("java/lang/Class") 执行时,classMap["java/lang/Class"] 还不存在(因为类还没加载完),所以下面那段"创建类对象"的代码(在 LoadClass 末尾)会跳过——这就是打破循环的地方。

等 java/lang/Class 加载完毕,再遍历 classMap,给所有已加载的类(包括 java/lang/Class 自己、以及它触发加载的 java/lang/Object、Serializable 等)补上类对象。

第二步:LoadClass() 末尾的补齐逻辑

func (self *ClassLoader) LoadClass(name string) *Class {
    if class, ok := self.classMap[name]; ok {
        return class                          // 已加载
    }
    var class *Class
    if name[0] == '[' {                        // 数组类
        class = self.loadArrayClass(name)
    } else {
        class = self.loadNonArrayClass(name)
    }
    if jlClassClass, ok := self.classMap["java/lang/Class"]; ok {
        class.jClass = jlClassClass.NewObject()   // ← 后加载的类,直接关联
        class.jClass.extra = class
    }
    return class
}

判断 java/lang/Class 是否已经加载——如果已加载(也就是 loadBasicClasses 之后),每次加载新类时立即关联类对象。

用图表示这个"自举"过程:

阶段一:NewClassLoader()
  └── loadBasicClasses()
        ├── LoadClass("java/lang/Class")
        │     ├── classMap 里没有 → loadNonArrayClass
        │     │     ├── 递归加载超类 java/lang/Object
        │     │     │     └── LoadClass("java/lang/Object")
        │     │     │           ├── loadNonArrayClass
        │     │     │           └── 末尾:classMap["java/lang/Class"] 不存在
        │     │     │                 → 跳过关联(Object.jClass = nil)
        │     │     ├── 递归加载接口 Serializable 等
        │     │     │     └── 同样跳过关联
        │     │     └── 解析完成,classMap["java/lang/Class"] = class
        │     └── 末尾:classMap["java/lang/Class"] 刚刚才有
        │           → 但此时 self.classMap 查询的是"加载前"的状态?
        │           (见下方说明)
        │
        └── 遍历 classMap,给所有 jClass == nil 的类补齐
              ├── java/lang/Class → 关联类对象 ✓
              ├── java/lang/Object → 关联类对象 ✓
              └── Serializable 等 → 关联类对象 ✓

阶段二:之后每次 LoadClass
        └── 末尾:classMap["java/lang/Class"] 已存在
              → 立即关联类对象 ✓

一个微妙的地方:LoadClass("java/lang/Class") 内部的 loadNonArrayClass 执行完后,代码会执行到 if jlClassClass, ok := self.classMap["java/lang/Class"] —— 此时 self.classMap["java/lang/Class"] 刚刚被 defineClass 写入了(第 6 章讲过,defineClass 的最后一步是 self.classMap[class.name] = class)。

所以严格来说,java/lang/Class 自己会在 LoadClass 末尾就完成关联。而 loadBasicClasses 里那个 for 循环,主要是给在它之前加载的类(如 java/lang/Object)补齐。

书里说"好啦,问题已经解决了一半"——这个"一半"指的是 java/lang/Object 等先行加载的类。这种"先跑一遍再回头补"的模式在系统启动代码里很常见。

三、基本类型的类:int.class 是什么

3.1 Java 里的写法

System.out.println(void.class.getName());      // void
System.out.println(int.class.getName());       // int
System.out.println(double.class.getName());    // double

void 和 8 种基本类型也有"类" —— 但它们不是普通的类,和数组类一样,由 JVM 运行时生成。

3.2 loadPrimitiveClasses

func (self *ClassLoader) loadPrimitiveClasses() {
    for primitiveType := range primitiveTypes {
        self.loadPrimitiveClass(primitiveType)   // primitiveType 是 void、int、float 等
    }
}

func (self *ClassLoader) loadPrimitiveClass(className string) {
    class := &Class{
        accessFlags: ACC_PUBLIC,
        name:        className,
        loader:      self,
        initStarted: true,
    }
    class.jClass = self.classMap["java/lang/Class"].NewObject()
    class.jClass.extra = class
    self.classMap[className] = class
}

注意 primitiveTypes 这个 map 里包含 "void" —— 所以总共生成 9 个类:void、boolean、byte、short、char、int、long、float、double。

书里专门列了三点:

第一,void 和基本类型的类名就是 void、int、float 等(不是 [I 那种描述符形式)。

第二,基本类型的类没有超类,也没有实现任何接口(和数组类不同,数组类的超类是 Object)。

第三,非基本类型的类对象是通过 ldc 指令加载的,而基本类型的类对象,虽然在 Java 代码里看起来是通过字面量获取的,但编译后的指令不是 ldc,而是 getstatic。

3.3 第三点:一个反直觉的发现

System.out.println(int.class);

你以为编译成 ldc?错了。看 Integer 类的源码:

public final class Integer extends Number implements Comparable<Integer> {
    @SuppressWarnings("unchecked")
    public static final Class<Integer> TYPE
        = (Class<Integer>) Class.getPrimitiveClass("int");
    // ...
}

int.class 编译后是:

getstatic  java/lang/Integer.TYPE : Ljava/lang/Class;

也就是说:

Java 写法编译成需要什么
String.classldcldc 支持 ClassRef(9.3.4 节改造)
int.classgetstatic Integer.TYPEClass.getPrimitiveClass() 本地方法
obj.getClass()invokevirtualObject.getClass() 本地方法

为什么 int.class 不用 ldc? 因为 ldc 只能加载常量池里存在的常量,而 class 文件常量池里没有 CONSTANT_Class 指向 int 这种基本类型(规范不允许)。所以 javac 只能用"访问包装类的静态字段"来实现。

这是个很典型的"语言特性靠标准库 + JVM 配合实现"的例子。

3.4 数组类的类对象呢

System.out.println(int[].class.getName());     // [I

int[].class 编译成 ldc(因为常量池里可以有 CONSTANT_Class 指向 [I)。走 loadArrayClass → LoadClass 末尾的关联逻辑。

四、4 个本地方法

4.1 Object.getClass():3 行代码

// ch09/native/java/lang/Object.go
package lang

import "jvmgo/ch09/native"
import "jvmgo/ch09/rtda"

func init() {
    native.Register("java/lang/Object", "getClass",
                    "()Ljava/lang/Class;", getClass)
}

// public final native Class<?> getClass();
func getClass(frame *rtda.Frame) {
    this := frame.LocalVars().GetThis()
    class := this.Class().JClass()
    frame.OperandStack().PushRef(class)
}

这是全书实现的第一个本地方法,书里详细解释了三行:

  1. frame.LocalVars().GetThis() —— 从局部变量表 0 号 slot 拿 this
  2. this.Class().JClass() —— 对象的类 → 该类的类对象(用上了 jClass 字段)
  3. frame.OperandStack().PushRef(class) —— 结果压栈,等 areturn 指令取走

GetThis() 是什么? 书里说:

GetThis() 方法其实就是调用 GetRef(0),不过为了提高代码的可读性,给 LocalVars 结构体添加了这个方法。

func (self LocalVars) GetThis() *heap.Object {
    return self.GetRef(0)
}

和第 051 篇的 TopFrame() vs CurrentFrame() 是同一个思路——语义命名 > 复用方法。

4.2 Class.getPrimitiveClass()

// ch09/native/java/lang/Class.go
func init() {
    native.Register("java/lang/Class", "getPrimitiveClass",
        "(Ljava/lang/String;)Ljava/lang/Class;", getPrimitiveClass)
    native.Register("java/lang/Class", "getName0",
        "()Ljava/lang/String;", getName0)
    native.Register("java/lang/Class", "desiredAssertionStatus0",
        "(Ljava/lang/Class;)Z", desiredAssertionStatus0)
}

// static native Class<?> getPrimitiveClass(String name);
func getPrimitiveClass(frame *rtda.Frame) {
    nameObj := frame.LocalVars().GetRef(0)        // ← 静态方法,从 0 号 slot 开始
    name := heap.GoString(nameObj)                // Java String → Go string
    loader := frame.Method().Class().Loader()
    class := loader.LoadClass(name).JClass()
    frame.OperandStack().PushRef(class)
}

注意 GetRef(0) 而不是 GetThis() ——因为 getPrimitiveClass 是静态方法,没有 this,第 0 号 slot 就是第一个参数 name。

LoadClass("int") 会命中 classMap(loadPrimitiveClasses 已经生成好了),然后 .JClass() 拿到它的类对象。

4.3 Class.getName0()

// private native String getName0();
func getName0(frame *rtda.Frame) {
    this := frame.LocalVars().GetThis()           // 类对象
    class := this.Extra().(*heap.Class)           // 类对象 → Class 结构体(反向!)
    name := class.JavaName()
    nameObj := heap.JString(class.Loader(), name)
    frame.OperandStack().PushRef(nameObj)
}

这里用到了 extra 字段的反向查找——this 是个 java.lang.Class 实例,this.Extra() 拿到它描述的那个 Class 结构体。

JavaName() vs Name():

func (self *Class) JavaName() string {
    return strings.Replace(self.name, "/", ".", -1)
}
方法返回用途
Name()java/lang/StringJVM 内部类名(斜杠分隔)
JavaName()java.lang.StringJava 语言的类名(点分隔)

书里明确说:

注意这里需要的是 java.lang.Object 这样的类名,而非 java/lang/Object。

为什么不直接存成点分隔? 因为 JVM 规范里 class 文件的内部名一律用斜杠(java/lang/Object),只有展示给 Java 程序时才转成点。第 6 章 className := strings.Replace(cmd.class, ".", "/", -1) 是反方向的转换(启动时把 -cp 后的点分隔类名转成斜杠)。

getName0 为什么叫 “0”? JDK 的命名习惯——getName0() 是 getName() 的本地实现,加个 0 后缀表示"底层原始版本":

public String getName() {
    String name = this.name;
    if (name == null)
        this.name = name = getName0();      // ← 懒加载 + 缓存
    return name;
}

又是缓存模式(和 String.hash 一样)。因为类对象不可变,名字可以安全缓存。

4.4 Class.desiredAssertionStatus0()

// private static native boolean desiredAssertionStatus0(Class<?> clazz);
func desiredAssertionStatus0(frame *rtda.Frame) {
    frame.OperandStack().PushBoolean(false)
}

书里一句带过:

本书不讨论断言。desiredAssertionStatus0() 方法把 false 推入操作数栈顶。

但为什么要实现它? 因为:

Character 类是基本类型 char 的包装类,它在初始化时会调用 Class.desiredAssertionStatus0() 方法,所以这个方法也需要实现。

Character 类的静态初始化块里有断言相关代码,而我们第 7 章实现了类初始化——所以一旦有代码用到 Character,<clinit> 就会执行,就会调这个本地方法。不实现就 UnsatisfiedLinkError。

这揭示了 jvmgo 的一个现实:JDK 类库的初始化链很深,你永远不知道哪个类的 <clinit> 会调到哪个本地方法。所以第 9 章要实现的本地方法清单,是实践中撞出来的,不是设计出来的。

五、改造 ldc:支持类对象

5.1 新增 case

// ch09/instructions/constants/ldc.go
func _ldc(frame *rtda.Frame, index uint) {
    stack := frame.OperandStack()
    class := frame.Method().Class()
    c := class.ConstantPool().GetConstant(index)
    switch c.(type) {
    case int32:   stack.PushInt(c.(int32))
    case float32: stack.PushFloat(c.(float32))
    case string:
        internedStr := heap.JString(class.Loader(), c.(string))
        stack.PushRef(internedStr)
    case *heap.ClassRef:                              // ← 新增
        classRef := c.(*heap.ClassRef)
        classObj := classRef.ResolvedClass().JClass()
        stack.PushRef(classObj)
    default: panic("todo: ldc!")
    }
}

String.class 的完整链路:

① javac 编译 String.class
   → ldc #2    // class java/lang/String

② class 文件常量池
   #2 = CONSTANT_Class_info { name_index: #30 }   // "java/lang/String"

③ 运行时常量池(第 6 章)
   cp.consts[2] = &heap.ClassRef{ className: "java/lang/String" }

④ ldc 执行(本篇)
   c := cp.GetConstant(2)              → *heap.ClassRef
   classRef.ResolvedClass()            → 加载/查找 Class 结构体
   .JClass()                           → 它的类对象
   stack.PushRef(classObj)

注意 ResolvedClass() 会触发类加载——所以 ldc SomeClass.class 会导致 SomeClass 被加载,但不会初始化(加载 ≠ 初始化,第 7 章讲过)。

这也是 Class.forName(name, false, loader)(不初始化)和 Class.forName(name)(初始化)的区别所在。

5.2 ldc 现在支持四类

常量池类型运行时类型压入说明
CONSTANT_Integerint32PushInt第 5 章
CONSTANT_Floatfloat32PushFloat第 5 章
CONSTANT_StringstringPushRef第 8 章(经 JString)
CONSTANT_Class*heap.ClassRefPushRef第 9 章(经 .JClass())

而 CONSTANT_Long / CONSTANT_Double 走 ldc2_w(第 5 章讲过,因为占两个 slot)。

六、测试:GetClassTest

书里给了一个覆盖全面的测试:

package jvmgo.book.ch09;

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;
    }
}

18 个 println,覆盖了全部三条路径:

类别例子走哪条路
基本类型(9 个)int.classgetstatic Integer.TYPE → Class.getPrimitiveClass()
普通类(2 个)Object.class、Runnable.classldc → ClassRef.ResolvedClass().JClass()
数组类(4 个)int[].class、Object[][].classldc → loadArrayClass → .JClass()
对象实例(3 个)"abc".getClass()、new double[0].getClass()invokevirtual → Object.getClass()

注意最后三个——"abc".getClass() 和 new double[0].getClass() 走 Object.getClass()(实例方法),验证的是 Object.class 字段的单向关系。

而 int[][].class.getName() 输出 [[I 而不是 [[I 加点——因为 getName() 对数组返回的是类型描述符形式,这是 JDK 的规定。

6.1 运行结果

$ go install jvmgo/ch09
$ javac -d . GetClassTest.java
$ ch09.exe -Xjre "D:\Java\jdk1.8.0\jre" jvmgo.book.ch09.GetClassTest
void
boolean
byte
char
short
int
long
float
double
java.lang.Object
[I
[[I
[Ljava.lang.Object;
[[Ljava.lang.Object;
java.lang.Runnable
java.lang.String
[D
[Ljava.lang.String;

全部正确。

6.2 一个值得注意的点

new String[0].getClass().getName() 输出 [Ljava.lang.String;。

为什么是点而不是斜杠? 因为 JDK 的 Class.getName() 对数组类型做了特殊处理:

// 真实的 JDK 8 实现(简化)
public String getName() {
    String name = this.name;
    if (name == null)
        this.name = name = getName0();
    return name;
}

getName0() 是本地方法,HotSpot 的实现里会把 / 替换成 .——但只替换类名部分,不替换 [ 前缀和描述符字符。

我们的实现是:

name := class.JavaName()      // 简单粗暴:全部 / → .

对 java/lang/String → java.lang.String ✓
对 [Ljava/lang/String; → [Ljava.lang.String; ✓

碰巧也对——因为数组描述符里只有类名部分含 /。

七、本篇代码清单

ch09/rtda/heap/
├── class.go           【改】新增 jClass 字段 + JClass() + JavaName()
├── object.go          【改】新增 extra 字段 + Extra()
├── class_loader.go    【改】NewClassLoader 调 loadBasicClasses/loadPrimitiveClasses
│                          loadBasicClasses / loadPrimitiveClasses / loadPrimitiveClass
│                          LoadClass 末尾关联 jClass
└── class_name_helper.go  (复用第 8 章的 primitiveTypes map)

ch09/instructions/
├── constants/ldc.go   【改】新增 case *heap.ClassRef
└── reserved/invokenative.go  【改】import _ "jvmgo/ch09/native/java/lang"

ch09/native/java/lang/
├── Object.go          【新】getClass()
└── Class.go           【新】getPrimitiveClass / getName0 / desiredAssertionStatus0

约 80 行新增代码,4 个本地方法。

本篇小结

第 9 章的第一批本地方法,围绕反射的最小可用集展开:

  1. 双向关系——Class.jClass(类 → 类对象)+ Object.extra(类对象 → 类)。extra 用 interface{} 是为了扩展性(第 10 章还要用它存栈帧信息)。有了这两个字段,getClass() 只需 3 行。
  2. 破"鸡生蛋"——loadBasicClasses() 先加载 java/lang/Class,再回头遍历 classMap 给所有已加载的类补上类对象;LoadClass() 末尾判断 java/lang/Class 是否已加载,是则立即关联。两阶段配合,所有类都有 jClass。
  3. 基本类型的类——loadPrimitiveClasses() 生成 void + 8 种基本类型的类(类名就是 int、void,无超类无接口)。int.class 编译后不是 ldc 而是 getstatic Integer.TYPE,因为 class 文件常量池不允许 CONSTANT_Class 指向基本类型。
  4. 4 个本地方法——Object.getClass()(3 行)、Class.getPrimitiveClass()(静态方法,从 slot 0 取参数)、Class.getName0()(用 extra 反向查找 + JavaName() 把 / 转成 .)、Class.desiredAssertionStatus0()(永远返回 false,因为 Character.<clinit> 会调它)。
  5. ldc 支持 ClassRef——String.class 走 ldc → ClassRef.ResolvedClass().JClass(),触发加载但不初始化。

下一篇(第 060 篇)继续填本地方法:字符串拼接和 String.intern(),以及 System.arraycopy()。这两个终于能让我们撕掉第 8 章留下的 StringBuilder 限制和 JString 构造 hack。


上一篇【第58篇】本地方法调用总览——打通 Java 与 C 的边界
下一篇【第60篇】基础类型包装类与自动装箱——Integer/Long 的秘密


转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/xyghehehehe/article/details/166883057

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--