FFM JNI

FFM JNI 一、JAVA调用上位机的动态链接库.dll的方式JNIFFMJava Native Interface(Java 本地接口)Foreign Function Memory API(外来函数内存接口)编写C胶水代码纯JAVA代码问题MinGW运行时DLL缺少DLL依赖。运行时可能会提示缺少libgcc_s_seh-1.dll、libstdc-6.dll、libwinpthread-1.dll等文件导致程序在其他电脑上无法启动。解决1.静态链接2.分发dll1.1 FFM必须要求 C 导出 extern C 函数FFM目标让Java直接调用extern C导出的函数。为什么你用 FFM 调用 DLL 时必须要求 C 导出 extern C 函数FFM 按原始函数名找函数C 默认会把名字加密extern C就是告诉编译器别加密。extern C 是 C 的语言链接规范在编译阶段告诉编译器以 C 语言的方式处理函数名和调用约定 避免 C 的 name mangling 名称修饰确保JavaFFM能通过原始函数名找到对应的入口点。1.C支持函数重载同名函数不同参数为了区分这些同名函数C 编译器做了名称修饰 → 导出的符号名 ≠ 你写的函数名2.FFM 按字面名字精确查找 → 找不到被修饰后的符号3.extern C 关闭名称修饰 → 符号名恢复为原始函数名FFM 就能找到了加了extern C之后C 语言不支持函数重载同一个编译单元里不能存在两个同名函数1.2FFM 中的“C 函数签名”C 函数签名Function Signature简单来说就是一个函数的“完整身份标识”。它告诉编译器和调用者这个函数叫什么名字、需要传入什么参数、会返回什么类型的数据。FFM 中的“C 函数签名” 函数名 返回值类型 参数类型列表由FunctionDescriptor精确描述是 Java 与 C 之间安全通信的“协议说明书”。JNI签名JNI 签名是一种 标准化的字符串格式 用于在 JNI 中唯一标识 Java 方法。因为Java 支持方法重载同名不同参数必须通过签名来区分。签名结构(参数类型签名)返回值类型签名C胶水代码JAVA与C之间并不兼容为了连接JAVA与C库必写的中间适配代码。什么是JNI胶水代码举个例子说明。在C端编写充当Java与C之间交互桥梁的这部分代码。实现Java和C的跨语言通信Java运行在JVMJava虚拟机JVM不直接运行源代码而是通过编译生成字节码JVM只运行字节码中C直接编译成机器码运行在操作系统层面。它们的执行环境完全隔离数据类型互不兼容无法直接进行通信。胶水代码通过JNI定义的一组标准接口来“粘合”实现跨语言调用。确定性释放指内存或资源在明确可知的、可预测的时间点被释放而不是依赖垃圾回收器在未来的某个不确定时刻回收。JVMJava Virtual MachineJAVA虚拟机JVM与JITjavac编译时生成.class字节码但是这个字节码是JVM的专属虚拟指令并不是物理计算机能看懂的二进制文件JVM 默认先用解释器边翻译边执行字节码而 JIT 即时编译器会监控重复执行的热点代码提前将其编译为当前硬件可识别的本地机器码并缓存后续再次执行时直接调用缓存机器码无需重复翻译提升运行性能。1.Java 代码不直接跑在操作系统上先编译成.class字节码JVM的“机器码”-二进制文件但是不是CPU-物理计算机能看懂的二进制2.JVM 是一层虚拟中间层负责把字节码翻译成当前系统能执行的指令3.实现跨平台一次编译到处运行Windows/Linux/Mac 各有对应版本 JVM同一套 class 文件全都能跑4.JIT是JVM的一部分。JITJVM 监控代码发现高频重复执行的代码直接一次性编译成本地机器码缓存起来后续调用直接跑机器码速度大幅提升。5.解释器JVM 用解释器逐条翻译字节码翻译成当前操作系统 CPU 能识别的本地机器码执行完就丢不保存。优缺点启动速度快频繁调用的方法反复翻译性能差。Java 调用 JNI 函数时每次跨越 Java → C边界都有固定成本JIT 编译器无法穿越这个边界进行优化。二、JDK IDEA安装IDEA安装教程配置java环境超详细_idea配置java零基础入门到精通收藏这篇就够了_idea安装教程及环境配置-CSDN博客三、IDEA下的mainsrc 下的 MainMain.java原代码文件--手动编写的未编译不能直接被JVM运行out/production/FMM_dll 下的 MainMain.class文件后缀名.class编译后的字节码文件JVM真正执行的文件Java虚拟机只能识别class字节码修改并运行src/Main.java时IDEA 会自动触发编译覆盖更新out/production/FMM_dll/Main.class。三.C DLL动态链接库文件tcp_comm_app与tcp_comm_framework生成动态链接库C核心共享库与C桥接共享库C桥接层共享库依赖C共享库tcp_comm_c_api.h 文件中的声明 就是 Java FFM 调用时需要的函数签名 。C DLL的函数签名--当前DLL要有可直接被Java FFM调用的函数签名问题Java FFM 只能调用 extern C 的纯 C 风格平坦函数 无法直接调用 C 类解决创建C桥接层用opaque handle 模式将 C 类包装成纯 C 函数接口opaque handle 不透明句柄外部使用者只能拿到句柄标识符看不到、无法直接访问底层内部数据结构所有操作必须通过库 / 模块提供的公开 API 完成。只能拿句柄提供的API接口去使用它但是你看不到它的内部结构一种强封装、信息隐藏的接口设计范式C 语言生态最常用。句柄对应的C对象存放在堆区.dll与.dll.a四、Java 调用JDK 21 --创建Maven配置文件pom.xml配置JDK 21 和 -enable-preview 参数IDEA右下角显示 Maven FMM_dll build scripts found说明IDEA还没有加载Maven项目配置。我需要让IDEA识别Maven项目。pom.xml文件pom.xml是Maven 项目的核心配置文件Project Object Model项目对象模型放在项目根目录Maven 依靠这个文件管理项目所有行为编译、JDK 版本、依赖、打包、构建命令等。代码说明作用这份pom.xml是你fmm-dll项目的构建规则文件核心作用是告诉 Maven 使用 Java25 标准编译源码适配 FFM 调用 DLL 的业务场景统一管控项目编译环境、编码与基础信息。1.项目基础坐标唯一标识2.统一JDK25配置3.build 构建配置手动指定整个项目Java源码的顶层根目录。4.编译插件--核心run.bat文件Windows 下的批处理启动脚本一键完成「清理→编译→运行」你的 Java FFM DLL 项目不用手动在 IDEA 点运行双击就能执行。调试时自动生成.xml文件不用学简单看GrepConsole.xml--IDEA 内置编译器配置告诉IDEA怎么编译你的Java代码。encodings.xml--记录项目所有文件的编码你项目统一 UTF-8解决中文乱码。GrepConsole--插件配置保存控制台日志的过滤规则和高亮配置jarRepositories.xml--记录项目中使用的 Maven 远程仓库地址misc.xml--杂项配置项目 SDK 版本、输出路径、全局环境参数。workspace.xml--工作量最大的本地配置打开了哪些文件、断点、Run/Debug 运行配置、窗口布局、光标位置、插件开关。五、FFM实现JDK25C下C桥接共享库tcp_comm_c_api.h 文件中的声明 就是 Java FFM 调用时需要的函数签名 。C的代码C封装层把底层C通信框架暴露为标准C接口允许Java调用该TCP通信库Java下创建.java--Java FFM绑定类封装所有C API调用这是一个 Java 通过 FFM (Foreign Function Memory) API 调用本地 C DLL 进行 TCP 通信 的测试项目。六、JNI实现1.问题问题1胶水代码.cpp为什么每个JNI函数都要加锁多余代码JAVA层是多线程需要加锁但是现在JAVA内是单线程不需要加锁。Java是单线程JNI函数串行调用不存在多个JNI函数同时访问g_jni_iocontext的情况不需要锁后台线程 control_thread_ 、 data_thread_ 、 thread_pool_ 只执行 io_context::run() 不修改 g_jni_context 中的智能指针不需要锁AppController 内部已经有 receive_mutex_ 保护消息队列不需要锁shuntdown销毁资源顺序先stop停止IO再join等待线程退出最后reset安全。操作顺序正确不需要锁问题2防止重复初始化方法方法优点缺点适用场景原子标志无锁开销高效需要 C11单线程/多线程都适用互斥锁标志线程安全简单有锁开销多线程场景RAII模式自动管理无需手动检查需要重构代码结构新项目推荐检查智能指针简单无需新增成员不够明确可能误判临时方案注意RAIIResource Acquisition Is Initialization资源获取即初始化把资源的申请放在类的构造函数 把资源的释放放在类的析构函数。 只要对象生命周期结束出作用域、函数 return、异常退出析构自动执行资源一定会释放杜绝泄漏。资源泛指所有需要手动释放的东西。问题3Lambda 捕获问题[](){}全局变量不会有生命周期问题[]引用捕获缺点[]隐式捕获所有变量不清楚捕获了什么若将g_jni_context改为局部变量线程执行时变量可能已经销毁会导致未定义行为2.JNI回调--通过JNI将回调注册到Java层JavaVM*JavaVM指针。指向Java虚拟机实例。JavaVM*就是 Java 虚拟机实例在 JNIC/C层面的指针或句柄。不需要关系具体指向只需要知道它是JVM的入口钥匙。Java进程生命周期中JavaVM*是单例的只有一个不管在多少C线程中调用它拿到的都是同一个指针。Java虚拟机实例本质上就是用java命令启动一个程序时在操作系统内存中创建的正在运行的JVM进程本身。承载着你所有 Java 代码运行的“容器”或“操作系统”。把Java 虚拟机实例想象成一个大型工厂3.JavaVM*vsJNIEnv*对比举例把Java 虚拟机实例想象成一个大型工厂工厂本身JavaVM*负责整个工厂的运营管理。它知道所有车间线程的位置能调配资源也能随时查看工厂的整体状态。车间主任JNIEnv*每个车间主任只负责自己的车间线程。他不能去别的车间发号施令。如果你想在 C 的某个新线程里干活必须通过工厂JavaVM*派一个该车间的主任AttachCurrentThread获取JNIEnv*给你。4.Java文件5、JNI跨语言通信字符串转换 Java 和 C 运行在不同环境必须通过 JNI API 进行转换才能互相访问数据将 Java 字符串转换为 C 字符串是为了java向C传数据C识别不出Java字符串所以用jni转为C字符串C 字符串转换为 Java 字符串是为了C返回结果给Java和JNI回调Java字符串识别不出C字符串七、FFMForeign Function Memory API与JNI1.FFM是JNI的替代方案与 JNI 相比FFM 带来的核心好处主要体现为开发效率和安全性的大幅提升纯Java开发模型告别复杂胶水代码JNI需要开发者编写繁琐的C/C桥接代码和头文件维护多套工具链工作量大且容易出错。而FFM API允许开发者在纯Java代码中直接描述和调用本地函数、操作本地内存省去了所有中间步骤。更高的安全性JNI中一个错误的指针操作就可能导致JVM崩溃。FFM API则对内存的分配、访问和释放提供了更严格的类型检查和生命周期管理通过MemorySession能有效预防悬垂指针、内存泄漏等常见且危险的错误。更标准、规范的APIJNI因其复杂性催生了JNA、JNR等第三方框架来解决部分问题但并未成为标准。FFM API是官方标准库的一部分拥有统一的编程模型。2. 替代后使用FFM是提高调用效率还是语言切换效率两者都有提升但角度和场景不同。总的来说FFM 提高了语言切换跨边界调用的效率且在长期、高频调用的场景下整体调用效率也更高。语言切换效率跨边界调用的“过路费”这是FFM优化最显著的地方。JNI的方法调用无法被JIT即时编译器内联等优化性能开销巨大。而FFM API构建在MethodHandle方法句柄之上这是一种为高效动态调用设计的VM虚拟机机制。这使得JIT编译器可以更好地优化跨边界调用**减少了Java与本地代码间上下文切换和状态转换的开销**。一份Apache Artemis项目的改进计划就指出FFM相比JNI有更低的开销Lower overhead。调用效率从“启动”到“长期运行”的表现这里的情况更有趣需要区分“启动阶段”和“稳定运行阶段”启动/预热阶段JNI更胜一筹。FFM在首次调用时需要初始化核心库并创建MethodHandle这会带来额外的启动成本约几十到上百毫秒。稳定运行/高频调用阶段FFM的优势完全体现出来。在完成预热后FFM的调用效率会超越JNI。一份来自OpenJDK社区的实测数据显示在达到约25万次调用的临界点后FFM的总体耗时开始反超JNI。在持续高频调用的场景下如UI渲染、高频计算FFM的性能优势会更明显典型表现为比JNI快约20%。RMS1344字节转换为float值共336组聚合方法--RMS--信号的等效直流能力反映了信号的强度或振幅RMS归一化均方根归一化为[0,1]八、WebSocketWebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工full-duplex通信即允许服务器主动发送信息给客户端。因此在WebSocket中浏览器和服务器只需要完成一次握手两者之间就直接可以创建持久性的连接并进行双向数据传输客户端和服务器之间的数据交换变得更加简单。旧方案否Java与前端怎么交互通过 WebSocket 协议Java是服务器端浏览器是客户端准备确认JDK版本为JDK 21C DLL导出函数的头文件DLL文件的位置Cpromise-future机制九、JNI各文件1.各个文件1.1.TcpCommJni.java — Java端JNI接口定义功能Java与C之间的桥梁接口定义了所有可以从Java调用的C本地方法。1.2.Main.java — Java端测试主程序功能独立的Java测试程序用于验证JNI桥接功能是否正常工作。1.3.jni_bridge.h — C端JNI头文件功能定义JNI函数签名和全局上下文结构体。1.4.jni_bridge.cpp — C端JNI实现功能定位 实现所有JNI函数负责Java与C之间的数据转换和调用转发。所有JNI函数都与AppController的方法一一对应除了 registerCallback() 是JNI层特有的用于保存Java回调对象引用其他所有JNI函数都直接调用了 AppController 的对应方法。2.JNI函数命名规则3.Main.java中创建JNI实例JNI是基于对象的。虽然native方法实际上操作的是C的全局单例g_jni_context但Java层面需要通过对象实例来调用native方法4.JNI回调机制回调你告诉别人“做完后通知我”JNI回调Java告诉C“当某个事件发生时调用我的这个方法”Java不会向C回调C向Java回调Java调用C是直接调用native方法不是回调。先注册回调函数等C端完成时通知Java端比如当C端控制通道连接成功回调触发通知Java端控制通道连接方法JNI回调C是跑腿的Java告诉C当某个事做完/当触发某个事件时C告诉Java做完某事连接完成/发送完成--主动操作的结果触发某个事件收到数据/设备断开--被动发生的事本质都是C调用Java回调方法5.回调转发6.回调与回调转发回调是直接调用回调转发是先处理再转发。在JNI场景下转发层负责语言转换、数据处理和线程安全。为什么要回调转发语言切换C的回调参数事C类型std::string不能直接传给Java方法数据处理C收到原始字节数据需要解析json格式和字节流后传给Java线程安全C回调在C线程中执行需要Attach到Java虚拟机才能调用Java方法6.匿名内部类匿名内部类没有名字定义在内部一次性使用的类不能复用不能有构造方法匿名内部类实现接口直接在参数位置创建不需要单独定义类直接在使用的地方创建7.注解--JavaOverride是Java的注解表示我要覆盖父类/接口的方法。告诉阅读者这是一个重写方法8.C调用JavaC异步工作需要通知Java事件9.如果没有C→Java回调会怎样Java只能 轮询 不断问C有没有数据轮询的问题 效率低 大部分时间都在空等延迟高 最多延迟100ms才能收到数据浪费资源 占用CPU和线程回调的优势 效率高 只有有数据时才处理零延迟 数据到达立即通知资源节约 不需要轮询线程10.JNI回调是做完某事还是触发某个事件两个都是只是对同一个意思不同描述。十、JNI基础1.JNIJNIJava Native Interface Java本地接口是Java提供的一种机制允许Java代码调用本地代码C/C也允许本地代码调用Java代码。2.JNI架构图3.Java端知识点3.1native方法声明native关键字表示方法实现不在Java中在本地代码中。3.2加载本地库3.3回调接口定义3.4匿名内部类实现回调3.5 Override注解4.C端知识点4.1JNI函数命名规则4.2JNI函数签名4.3 extern “C”导出为什么需要extern C C编译器会对函数名进行修饰如 _Z4funcv Java虚拟机按照 Java_包名_类名_方法名 的规则查找函数如果没有extern C函数名会被修饰Java找不到4.4JNI数据类型转换4.5JNI全局上下文JNI全局上下文是一个全局结构体存储了JNI桥接层需要的所有状态和资源。为什么需要全局上下文JNI函数是独立的需要一个共享的状态容器。5.JNI回调机制5.1回调注册为什么需要全局引用 Java对象默认是局部引用函数返回后会被垃圾回收回调对象需要在C线程中长期使用必须创建全局引用使用完后必须删除全局引用否则内存泄漏5.2C向Java回调5.3 调用Java方法的辅助函数5.4 方法签名格式jni_bridge.cpp方法签名为了让JNI找到正确的Java方法Java支持方法重载同名不同参数JNI需要方法签名来区分。6.JNI线程安全6.1JNIEnv的线程相关性JNIEnv* 是线程相关的每个线程有自己的JNIEnv不能跨线程使用同一个JNIEnv应该在在C线程中获取当前线程的JNIEnv6.1.1AttachCurrentThread()与DetachCurrentThread()是一个 JNI 函数用于将当前 native 线程连接attach到 Java 虚拟机JVM从而让该线程能够进行 JNI 调用访问 Java 对象和方法。AttachCurrentThread将C线程附加到Java虚拟机JVM获取该线程的JNIEnv*。AttachCurrentThread()和DetachCurrentThread()必须成对调用否则会导致线程资源泄漏甚至 JVM 崩溃。任何本地线程调用Java方法都必须先attach获取自己的JNIEnv。本项目每次回调都attach / detachAttach注册线程到JVMDetach解除注册仅首次需要Attach。6.1.2DeleteLocalRef释放局部引用防止JNI局部引用表溢出。为什么需要DeleteLocalRef防止局部引用泄漏导致JNI局部引用表溢出6.2JavaVM的全局共享JavaVM* 是全局共享的可以跨线程使用在JNI_OnLoad中保存JavaVM指针6.3全局引用与局部引用7.JNI生命周期初始化和关闭顺序有严格要求错误的顺序会导致崩溃、内存泄漏或资源未释放。7.1初始化流程初始化顺序依赖关系7.2关闭流程关闭顺序重要停止控制器 → 释放工作守卫 → 停止IO上下文 → 等待线程 → 删除全局引用 → 重置状态关闭顺序7.3总结初始化先基础设施后业务1. IO上下文 → 2. 线程池 → 3. 控制器 → 4. 初始化控制器 → 5. 注册回调 → 6. 工作守卫 → 7. 启动线程关闭先业务后基础设施1. 关闭控制器 → 2. 释放工作守卫 → 3. 停止IO上下文 → 4. 等待线程 → 5. 删除全局引用 → 6. 重置状态关闭工厂1.告诉工人停止工作关闭控制器2.等待工人离开工厂等待线程结束3.拆楼删除IO上下文关闭错误7.4 g_jni_contextg_jni_context是全局上下文对象包含了JNI桥接层需要的所有状态和资源。g_jni_context.control_io_context_ std::make_uniqueboost::asio::io_context();control_io_context_是std::make_uniqueboost::asio::io_context类型是C的Boost.Asio库不是JNI方法。7.5 控制器AppController整个C业务逻辑的核心。控制器依赖IO上下文控制器需要进行异步网络通信初始化注册回调用到控制器控制器是业务逻辑层提供了回调注册接口。不用IO上下文IO上下文是底层事件循环不是提供业务回调接口。7.6 IO上下文启动事件循环IO上下文在调用run()方法后真正启动。7.7 工作守卫工作守卫防止IO上下文因为没有任务而退出。为什么要加入工作守卫启动事件循环当前没有异步操作io.run()会立即返回线程结束后续的异步操作无法处理。加入io.context()不会返回因为工作守卫保持io_context“有工作”了即使当前没有异步操作io.run()也会等待线程持续运行。7.8总结7.9 make_unique / make_sharedmake_unique→ 独占、轻量、无计数首选make_shared→ 共享所有权、带引用计数、支持 weak_ptr连续内存分配更快但内存回收有延迟8.JNI编译与链接8.1 DLL编译8.2 Java头文件生成9.常见错误与注意9.1常见错误9.2注意使用全局引用保存跨线程使用的Java对象在非Java线程中调用Java方法时必须先AttachCurrentThread使用完局部引用后及时DeleteLocalRefJNI函数名必须严格按照Java_包名_类名_方法名格式使用extern C导出JNI函数关闭时按照正确的顺序释放资源10.为什么要有JNI桥接层解耦 AppController不需要知道Java的存在转换 处理跨语言数据类型转换线程安全 处理跨线程调用问题生命周期管理 统一管理资源的创建和释放11.回调驱动设计Java不需要轮询而是等待C通知事件驱动数据到达 → C回调 → Java处理 → 更新前端JNI与FFMJNI缺点为什么要用FFMJava线程分类Tomcat线程Tomcat是Spring Boot默认的嵌入式Web服务器Tomcat线程就是处理HTTP请求的工作线程。FFM各文件1.tcp_comm_c_api.h - C API 头文件extern C作用 定义 C 语言导出接口作为 Java FFI 和 C 实现之间的桥梁。2.tcp_comm_c_api.cpp - C API 实现作用 实现 C API 接口内部封装 AppController 管理双通道控制通道 数据通道的生命周期和数据流转。3.TcpCommBridge.java - Java FFI 桥接层作用 使用 Java 25 FFMForeign Function Memory API调用 C DLL提供类型安全的 Java API 包装。十一、FFM基础1.Linker链接器--FFM核心入口Java与原生代码之间的桥梁负责调用C函数将C函数签名与Java方法对应2.SymbolLookup符号查找--定位DLL中的函数作用 从加载的原生库中查找导出符号函数名。3.FunctionDescriptor函数符号描述--定义C函数签名作用 用Java类型描述C语言的参数和返回值类型FFM类型安全的基础。4.ValueLayout值布局--类型映射表作用 定义 Java 类型与C类型在内存中的布局映射关系。FFM类型安全的关键。注意 C 头文件中的 bool 映射到 Java 的 ValueLayout.JAVA_BOOLEAN 而不是 JAVA_BYTE 。5.MemorySegment内存段--原生内存的Java表示--原生内存块核心类作用 表示一块原生内存区域是 Java 和 C 之间共享数据的核心。表示原生对象的句柄从C返回的指针C字符串的内存区域任意原生内存缓冲区原生内存区域是 物理存在的内存块 MemorySegment 是 Java 世界用来 安全访问这块内存的带类型安全的代理对象 。没有 MemorySegmentJava 无法以类型安全的方式读写原生内存。6.Arena内存分配器--临时内存管理6.1Arena内存分配器--临时内存管理作用 管理 MemorySegment 的生命周期避免内存泄漏。负责分配和释放与原生代码交互所需要的临时内存。关键特性 - Arena.ofConfined() 创建受限区域自动管理内存- try-with-resources 退出作用域时自动释放所有分配的内存- 解决了旧 JNI 中手动管理 malloc/free 的痛点6.2内存生命周期与Resource Leak 防护两种内存来源 1. Arena 分配的临时内存 try-with-resources 自动释放2. C 端 strdup/new 分配的内存 需要显式调用 tcp_comm_free_string 或对应释放函数关键 C 头文件中如果有返回 char* 或其他需要释放的资源必须提供对应的释放 API。try-with-resources 它确保在 try 块执行完毕后 无论是否发生异常 都会自动调用资源的 close() 方法来释放资源。问题每个 Arena.ofShared() 都会创建一个新的共享Arena且全程没有调用 close() 。影响原生内存泄漏每个stub占用的资源无法释放持续累积内存泄露改进复用stub显示管理Shared Arena的生命周期将所有Stub创建在同一个Shared Arena中在 globalShutdown() 时关闭Arena适合本项目所有handle共享同一个Stub7.MethodHandle与Handle7.1MethodHandle作用 封装原生函数调用提供类型安全的调用接口。7.2HandleC 端通过 void* 表示内部对象Java 端通过 MemorySegment 表示这个指针。这是跨语言状态管理的核心模式。8.Java String 到 C char* 的转换作用 将 Java String 转换为 C 兼容的 null 终止字符串。FFM不自动处理String需要手动编码。9.缓冲区传递模式Buffer-Passing作用 Java 预先分配缓冲区C 侧填充数据后 Java 读取。设计原因 避免 C 返回分配的字符串导致内存泄漏符合项目约束 tcp_comm_dual_get_last_frame_hex 必须使用缓冲区传递10.句柄检查Handle Validation作用 防止空指针和无效句柄导致的崩溃。11.Downcall与Upcall向下调用与向上调用11.1向下调用Java 调用 DLLJava调用方C被调用方。通过 MethodHandle.invoke() 从 Java 直接调用 C 函数。11.2向上调用DLL调用Java C调用方Java 被调用方实现真正的FFM upcall机制12.DLL加载机制Java 通过 System.load() 加载原生库必须指定完整路径。13.静态初始化块中的绑定注册所有 FFM 绑定注册在 static {} 块中完成确保类加载时一次性建立所有原生调用桥。14.Stub回调跳板Stub桩函数 是 FFM中用于实现 C 调用 Java 回调的关键机制。stubC接口外壳Java实现内核。stub解决C无法直接调用Java方法需要一个C语言接口。C只能理解C函数指针无法理解Java函数引用。生命周期Arena创建手动close释放Stub 不是普通的 Java 方法而是通过 LINKER.upcallStub() 动态生成的特点它是一块分配在 原生内存 中的代码符合 C 调用约定 栈布局、参数传递、返回值内部包含 JVM 调用桥接逻辑。GC垃圾回收GC 是 JVM 自动清理无效 Java 对象、回收内存的机制它会产生 STW 停顿是 JNI/FFM 性能测试中长尾延迟最重要的干扰因素之一。P50中位数代表普通水平看不到长尾P99 / P999专门用来捕捉长尾延迟P99 越高说明尾巴拖得越长系统抖动越严重。你表格里的现象完美印证 每组 1000 个样本单独统计时P99 只有 262μs 合并 10000 条全局统计P99 直接涨到 524μs。 原因样本量变多更容易采集到罕见的长尾慢请求长尾效应显现。