自己动手写 Java 虚拟机笔记 - 第九部分:本地方法调用与反射机制实现

自己动手写 Java 虚拟机笔记 - 第九部分:本地方法调用与反射机制实现

Luca Ju
2025-07-04 / 0 评论 / 7 阅读 / 正在检测是否收录...

前言

在前一章中,我们实现了数组和字符串的核心机制,完善了 JVM 对复杂数据结构的支持。本章将聚焦 本地方法调用 与反射机制 —— 本地方法(native 方法)是 Java 与底层系统交互的桥梁(如调用操作系统 API、硬件驱动等),而反射机制则依赖本地方法实现类信息的动态访问(如动态获取类结构、调用方法)。本章将通过 Go 语言模拟本地方法的注册、调用逻辑,实现反射的核心功能,并验证关键场景(如字符串拼接、类信息获取),让 JVM 具备与底层交互和动态操作类的能力。

参考资料

《自己动手写 Java 虚拟机》—— 张秀宏

开发环境

工具 / 环境版本说明
操作系统MacOS 15.5基于 Intel/Apple Silicon 均可
JDK1.8用于字节码分析和测试
Go 语言1.23.10项目开发主语言

第九章:本地方法调用与反射机制

本地方法是 Java 语言扩展能力的关键,允许开发者通过其他语言(如 C/C++)实现底层功能;反射则基于本地方法实现类信息的动态访问。本章将从本地方法的注册、调用逻辑入手,逐步实现反射机制,并验证核心场景的正确性。

一、本地方法基础:注册与调用机制

本地方法(native 方法)没有 Java 字节码实现,需通过外部语言实现并注册到 JVM 中。JVM 需提供注册机制和调用逻辑,确保能正确找到并执行本地方法。

1. 本地方法注册:建立方法映射表

本地方法通过 “类名 + 方法名 + 方法描述符” 唯一标识,使用 map 存储方法映射关系(key 为标识,value 为 Go 实现的函数)。

// NativeMethod 定义本地方法的函数类型(接收栈帧,无返回值)
type NativeMethod func(frame *rtda.Frame)

// registry 存储本地方法映射:key 为 "类名~方法名~描述符",value 为本地方法实现
var registry = map[string]NativeMethod{}

// Register 注册本地方法
func Register(className string, methodName string, methodDescriptor string, method NativeMethod) {
    key := className + "~" + methodName + "~" + methodDescriptor
    registry[key] = method
}

key 设计逻辑

  • 类名、方法名、描述符共同构成唯一标识,避免不同类中同名方法的冲突(如 java/lang/System.arraycopyjava/util/Arrays.arraycopy 需区分)。
  • 示例:java/lang/System~arraycopy~(Ljava/lang/Object;ILjava/lang/Object;II)V 标识 System.arraycopy 方法。

2. 本地方法调用:从字节码到本地实现

JVM 通过 invokenative 指令调用本地方法,核心流程为:解析方法标识→查找本地实现→执行本地函数。

(1)注入本地方法的 “伪字节码”

本地方法无 Code 属性,需为其注入最小化字节码(用于解释器流程兼容):

// injectCodeAttribute 为本地方法注入伪 Code 属性
func (m *Method) injectCodeAttribute(returnType string) {
    m.maxStack = 4 // 操作数栈默认深度
    m.maxLocals = m.argSlotCount // 局部变量表大小=参数槽数
    // 根据返回类型生成伪字节码(首字节 0xFE 标识本地方法,第二字节为返回指令)
    switch returnType[0] {
    case 'V': // void 返回
        m.code = []byte{0xfe, 0xb1} // 0xFE=本地方法标识,0xB1=return 指令
    case 'D': // double 返回
        m.code = []byte{0xfe, 0xaf} // 0xAF=dreturn 指令
    case 'F': // float 返回
        m.code = []byte{0xfe, 0xae} // 0xAE=freturn 指令
    case 'J': // long 返回
        m.code = []byte{0xfe, 0xad} // 0xAD=lreturn 指令
    case 'L', '[': // 引用类型返回
        m.code = []byte{0xfe, 0xb0} // 0xB0=areturn 指令
    default: // 基本类型(int/short等)返回
        m.code = []byte{0xfe, 0xac} // 0xAC=ireturn 指令
    }
}

设计目的:确保解释器能正常解析方法结构,通过 0xFE 标识触发本地方法调用逻辑。

(2)invokenative 指令执行逻辑
// INVOKE_NATIVE 调用本地方法的指令
type INVOKE_NATIVE struct {
    base.NoOperandsInstruction
}

func (i *INVOKE_NATIVE) Execute(frame *rtda.Frame) {
    method := frame.Method()
    className := method.Class().Name()
    methodName := method.Name()
    descriptor := method.Descriptor()

    // 查找本地方法实现
    nativeMethod := native.FindNativeMethod(className, methodName, descriptor)
    if nativeMethod == nil {
        // 未找到本地方法时抛出异常
        panic("java.lang.UnsatisfiedLinkError: " + className + "." + methodName + descriptor)
    }

    // 执行本地方法
    nativeMethod(frame)
}

// FindNativeMethod 从注册表查找本地方法
func FindNativeMethod(className, methodName, descriptor string) NativeMethod {
    key := className + "~" + methodName + "~" + descriptor
    if method, ok := registry[key]; ok {
        return method
    }
    // 特殊处理:对未实现的 native 方法返回默认实现(如 Object.registerNatives)
    if methodName == "registerNatives" && descriptor == "()V" {
        return func(frame *rtda.Frame) {} // 空实现
    }
    return nil
}

调用流程

  1. 从当前栈帧获取方法的类名、方法名、描述符;
  2. 生成 key 并查找本地方法实现;
  3. 执行找到的本地函数(传入栈帧,操作局部变量和操作数栈)。

二、反射机制实现:基于本地方法的动态类访问

反射允许程序在运行时动态获取类信息(如类名、方法、字段)并操作,其核心依赖 java/lang/Class 类(类对象)和相关本地方法。

1. 类对象(java/lang/Class 实例)的绑定

每个类在 JVM 中对应唯一的 Class 实例(类对象),存储类的元信息,是反射的入口。

// Class 结构体新增类对象字段
type Class struct {
    // ... 原有字段 ...
    jClass *Object // 对应的 java/lang/Class 实例(类对象)
}

// 类加载时绑定类对象
func (c *ClassLoader) LoadClass(name string) *Class {
    // ... 原有加载逻辑 ...
    // 绑定类对象:当 java/lang/Class 类已加载时
    if jlClassClass, ok := c.classMap["java/lang/Class"]; ok {
        class.jClass = jlClassClass.NewObject() // 创建 Class 实例
        class.jClass.extra = class // 关联到当前类(通过 extra 字段存储元信息)
    }
    return class
}

类对象的作用

  • 作为反射的入口(如 obj.getClass() 返回类对象);
  • 存储类的元信息(通过 extra 字段关联到 JVM 内部的 Class 结构体)。

2. 核心反射本地方法实现

反射的关键操作(如获取类名、获取类对象)依赖本地方法实现,以下是核心方法的 Go 实现。

(1)Object.getClass():获取对象的类对象
// 注册本地方法:java/lang/Object.getClass()
func init() {
    native.Register("java/lang/Object", "getClass", "()Ljava/lang/Class;", getClass)
}

// getClass 实现:返回对象的类对象
func getClass(frame *rtda.Frame) {
    this := frame.LocalVars().GetThis() // 获取当前对象(this)
    class := this.Class().JClass()     // 获取类对象(jClass 字段)
    frame.OperandStack().PushRef(class) // 推送类对象到操作数栈
}
(2)Class.getName0():获取类的名称
// 注册本地方法:java/lang/Class.getName0()
func init() {
    native.Register("java/lang/Class", "getName0", "()Ljava/lang/String;", getName0)
}

// getName0 实现:返回类的全限定名
func getName0(frame *rtda.Frame) {
    this := frame.LocalVars().GetThis()       // 获取 Class 实例(类对象)
    class := this.Extra().(*heap.Class)       // 从 extra 字段获取 JVM 内部 Class 结构体
    name := class.JavaName()                  // 转换类名为 Java 格式(如 "[I" → "int[]")
    jString := heap.JString(class.Loader(), name) // 转换为 Java String 对象
    frame.OperandStack().PushRef(jString)     // 推送结果到操作数栈
}

// JavaName 将 JVM 类名转换为 Java 规范名称
func (c *Class) JavaName() string {
    if c.IsArray() {
        return c.name // 数组类名已符合规范(如 "[I")
    }
    return strings.ReplaceAll(c.name, "/", ".") // 普通类名:"java/lang/String" → "java.lang.String"
}
(3)Class.getPrimitiveClass():获取基本类型的类对象
// 注册本地方法:java/lang/Class.getPrimitiveClass()
func init() {
    native.Register("java/lang/Class", "getPrimitiveClass", "(Ljava/lang/String;)Ljava/lang/Class;", getPrimitiveClass)
}

// getPrimitiveClass 实现:返回基本类型的类对象
func getPrimitiveClass(frame *rtda.Frame) {
    vars := frame.LocalVars()
    nameObj := vars.GetRef(0) // 获取基本类型名称(如 "int")
    name := heap.GoString(nameObj) // 转换为 Go 字符串

    loader := frame.Method().Class().Loader()
    var class *heap.Class
    switch name {
    case "void":
        class = loader.LoadClass("void")
    case "boolean":
        class = loader.LoadClass("boolean")
    // ... 其他基本类型 ...
    default:
        panic("Invalid primitive type: " + name)
    }

    frame.OperandStack().PushRef(class.JClass()) // 推送基本类型的类对象
}

三、核心本地方法案例:数组拷贝与字符串操作

除反射外,Java 类库中的许多基础功能依赖本地方法,如数组拷贝、字符串拼接等。以下实现关键场景的本地方法。

1. System.arraycopy():数组拷贝

// 注册本地方法:java/lang/System.arraycopy()
func init() {
    native.Register("java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", arraycopy)
}

// arraycopy 实现:数组元素拷贝
func arraycopy(frame *rtda.Frame) {
    vars := frame.LocalVars()
    src := vars.GetRef(0)    // 源数组
    srcPos := vars.GetInt(1) // 源数组起始位置
    dest := vars.GetRef(2)   // 目标数组
    destPos := vars.GetInt(3)// 目标数组起始位置
    length := vars.GetInt(4) // 拷贝长度

    // 校验:源/目标数组非空
    if src == nil || dest == nil {
        panic("java.lang.NullPointerException")
    }
    // 校验:数组类型兼容
    if !checkArrayCopy(src, dest) {
        panic("java.lang.ArrayStoreException")
    }
    // 校验:索引不越界
    if srcPos < 0 || destPos < 0 || length < 0 ||
        srcPos+length > src.ArrayLength() ||
        destPos+length > dest.ArrayLength() {
        panic("java.lang.IndexOutOfBoundsException")
    }

    // 执行拷贝(根据数组类型调用对应拷贝逻辑)
    heap.ArrayCopy(src, dest, srcPos, destPos, length)
}

// 校验数组拷贝的类型兼容性
func checkArrayCopy(src, dest *heap.Object) bool {
    srcClass, destClass := src.Class(), dest.Class()
    // 必须都是数组
    if !srcClass.IsArray() || !destClass.IsArray() {
        return false
    }
    // 基本类型数组必须类型相同;引用类型数组允许子类向父类拷贝
    if srcClass.ComponentClass().IsPrimitive() || destClass.ComponentClass().IsPrimitive() {
        return srcClass == destClass // 基本类型数组必须同类型
    }
    return true // 引用类型数组兼容
}

2. 字符串拼接与 String.intern()

字符串拼接依赖 StringBuilder.append(),而 append 又依赖 System.arraycopyString.intern() 则依赖字符串池实现常量共享。

(1)String.intern():字符串驻留
// 注册本地方法:java/lang/String.intern()
func init() {
    native.Register("java/lang/String", "intern", "()Ljava/lang/String;", intern)
}

// intern 实现:将字符串驻留到字符串池
func intern(frame *rtda.Frame) {
    this := frame.LocalVars().GetThis() // 当前 String 对象
    interned := heap.InternString(this) // 从字符串池获取驻留的字符串
    frame.OperandStack().PushRef(interned) // 推送结果
}

// InternString 实现字符串驻留
func InternString(jStr *Object) *Object {
    goStr := GoString(jStr) // 从 String 对象获取 Go 字符串
    // 检查字符串池,存在则返回,否则添加
    if interned, ok := internedStrings[goStr]; ok {
        return interned
    }
    internedStrings[goStr] = jStr
    return jStr
}

四、功能测试

通过测试案例验证本地方法和反射机制的正确性。

1. 反射测试:ClassTest 验证类名获取

测试目标:通过反射获取基本类型、数组、普通类的类名。

public class ClassTest {
    public static void main(String[] args) {
        System.out.println(void.class.getName());       // void
        System.out.println(boolean.class.getName());    // boolean
        System.out.println(int[].class.getName());      // [I
        System.out.println(Object.class.getName());     // java.lang.Object
        System.out.println("abc".getClass().getName()); // java.lang.String
    }
}

测试结果:正确输出各类的规范名称,验证 getClass()getName0() 等本地方法正常工作。

ch09-reflecttest.png

2. 字符串测试:StrTest 验证 intern() 机制

测试目标:验证字符串池的驻留机制(intern() 后相同内容字符串引用相同)。

public class StrTest {
    public static void main(String[] args) {
        String s1 = "abc1";
        String s2 = "abc1";
        System.out.println(s1 == s2);   // true(常量池相同引用)
        
        int x = 1;
        String s3 = "abc" + x; // 动态拼接,初始不在常量池
        System.out.println(s1 == s3);   // false
        
        s3 = s3.intern(); // 驻留到字符串池
        System.out.println(s1 == s3);   // true(引用相同)
    }
}

测试结果:输出符合预期,验证 intern() 方法和字符串池机制正确。

ch09-strtest.png

本章小结

本章实现了本地方法调用和反射机制的核心逻辑,重点包括:

  1. 本地方法框架:通过注册表(map)管理本地方法,注入伪字节码支持解释器流程,实现 invokenative 指令调用逻辑;
  2. 反射机制:绑定类对象(java/lang/Class 实例)与类元信息,实现 getClass()getName0() 等核心反射本地方法;
  3. 关键本地方法:实现 System.arraycopy()(数组拷贝)、String.intern()(字符串驻留)等类库依赖的本地方法;
  4. 功能验证:通过反射类名测试和字符串驻留测试,验证本地方法和反射机制的正确性。

本地方法和反射是 Java 灵活性的重要支撑,下一章将完善异常处理机制,使 JVM 能更健壮地处理运行时错误。

源码地址:https://github.com/Jucunqi/jvmgo.git
1

评论 (0)

取消