上一篇【第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;
// ...
}
不行,因为:
Class类的实例变量有很多(name、classLoader、componentType、cachedConstructor等),我们只实现一个子集- 有些字段是 JDK 版本相关的(JDK 8 vs JDK 9 结构不同)
- 最关键的:从
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.class | ldc | ldc 支持 ClassRef(9.3.4 节改造) |
int.class | getstatic Integer.TYPE | Class.getPrimitiveClass() 本地方法 |
obj.getClass() | invokevirtual | Object.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)
}
这是全书实现的第一个本地方法,书里详细解释了三行:
frame.LocalVars().GetThis()—— 从局部变量表 0 号 slot 拿thisthis.Class().JClass()—— 对象的类 → 该类的类对象(用上了jClass字段)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/String | JVM 内部类名(斜杠分隔) |
JavaName() | java.lang.String | Java 语言的类名(点分隔) |
书里明确说:
注意这里需要的是
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_Integer | int32 | PushInt | 第 5 章 |
CONSTANT_Float | float32 | PushFloat | 第 5 章 |
CONSTANT_String | string | PushRef | 第 8 章(经 JString) |
CONSTANT_Class | *heap.ClassRef | PushRef | 第 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.class | getstatic Integer.TYPE → Class.getPrimitiveClass() |
| 普通类(2 个) | Object.class、Runnable.class | ldc → ClassRef.ResolvedClass().JClass() |
| 数组类(4 个) | int[].class、Object[][].class | ldc → 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 章的第一批本地方法,围绕反射的最小可用集展开:
- 双向关系——
Class.jClass(类 → 类对象)+Object.extra(类对象 → 类)。extra用interface{}是为了扩展性(第 10 章还要用它存栈帧信息)。有了这两个字段,getClass()只需 3 行。 - 破"鸡生蛋"——
loadBasicClasses()先加载java/lang/Class,再回头遍历classMap给所有已加载的类补上类对象;LoadClass()末尾判断java/lang/Class是否已加载,是则立即关联。两阶段配合,所有类都有jClass。 - 基本类型的类——
loadPrimitiveClasses()生成void+ 8 种基本类型的类(类名就是int、void,无超类无接口)。int.class编译后不是ldc而是getstatic Integer.TYPE,因为 class 文件常量池不允许CONSTANT_Class指向基本类型。 - 4 个本地方法——
Object.getClass()(3 行)、Class.getPrimitiveClass()(静态方法,从 slot 0 取参数)、Class.getName0()(用extra反向查找 +JavaName()把/转成.)、Class.desiredAssertionStatus0()(永远返回 false,因为Character.<clinit>会调它)。 - 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




