一、JAVA调用上位机的动态链接库.dll的方式
| JNI | FFM |
| Java 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.分发dll
1.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运行在JVM(Java虚拟机,JVM不直接运行源代码,而是通过编译生成字节码,JVM只运行字节码)中,C++直接编译成机器码,运行在操作系统层面。它们的执行环境完全隔离,数据类型互不兼容,无法直接进行通信。胶水代码通过JNI定义的一组标准接口来“粘合”,实现跨语言调用。 - 确定性释放:指内存或资源在明确可知的、可预测的时间点被释放,而不是依赖垃圾回收器在未来的某个不确定时刻回收。
- JVM:Java Virtual Machine(JAVA虚拟机)
- JVM与JIT
javac编译时生成.class字节码,但是这个字节码是JVM的专属虚拟指令,并不是物理计算机能看懂的二进制文件,JVM 默认先用解释器边翻译边执行字节码,而 JIT 即时编译器会监控重复执行的热点代码,提前将其编译为当前硬件可识别的本地机器码并缓存,后续再次执行时直接调用缓存机器码,无需重复翻译,提升运行性能。
1.Java 代码不直接跑在操作系统上,先编译成.class字节码(JVM的“机器码”-二进制文件,但是不是CPU-物理计算机能看懂的二进制);
2.JVM 是一层虚拟中间层,负责把字节码翻译成当前系统能执行的指令;
3.实现跨平台:一次编译,到处运行Windows/Linux/Mac 各有对应版本 JVM,同一套 class 文件全都能跑
4.JIT是JVM的一部分。
JIT:JVM 监控代码,发现高频重复执行的代码,直接一次性编译成本地机器码缓存起来,后续调用直接跑机器码,速度大幅提升。
5.解释器:JVM 用解释器逐条翻译字节码,翻译成当前操作系统 CPU 能识别的本地机器码,执行完就丢,不保存。
优缺点:启动速度快;频繁调用的方法反复翻译,性能差。 - Java 调用 JNI 函数时,每次跨越 Java → C边界都有固定成本,JIT 编译器无法穿越这个边界进行优化。
二、JDK IDEA安装
IDEA安装教程配置java环境(超详细)_idea配置java,零基础入门到精通,收藏这篇就够了_idea安装教程及环境配置-CSDN博客
三、IDEA下的main
src 下的 Main(Main.java):原代码文件--手动编写的,未编译,不能直接被JVM运行
out/production/FMM_dll 下的 Main(Main.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实现
- JDK25
- C++下: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:防止重复初始化方法
| 方法 | 优点 | 缺点 | 适用场景 |
| 原子标志 | 无锁开销,高效 | 需要 C++11+ | 单线程/多线程都适用 |
| 互斥锁+标志 | 线程安全,简单 | 有锁开销 | 多线程场景 |
| RAII模式 | 自动管理,无需手动检查 | 需要重构代码结构 | 新项目推荐 |
| 检查智能指针 | 简单,无需新增成员 | 不够明确,可能误判 | 临时方案 |
- 注意:RAII(Resource Acquisition Is Initialization):资源获取即初始化
把资源的申请放在类的构造函数, 把资源的释放放在类的析构函数。 只要对象生命周期结束(出作用域、函数 return、异常退出),析构自动执行,资源一定会释放,杜绝泄漏。
资源泛指所有需要手动释放的东西。
问题3:Lambda 捕获问题[](){}
- 全局变量不会有生命周期问题
- [&]引用捕获缺点:
- [&]隐式捕获所有变量,不清楚捕获了什么
- 若将g_jni_context改为局部变量,线程执行时变量可能已经销毁,会导致未定义行为
2.JNI回调--通过JNI将回调注册到Java层
JavaVM*:JavaVM指针。指向Java虚拟机实例。
JavaVM*就是 Java 虚拟机实例在 JNI(C/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++字符串
七、FFM(Foreign Function & Memory API)与JNI
1.FFM是JNI的替代方案
与 JNI 相比,FFM 带来的核心好处主要体现为开发效率和安全性的大幅提升:
纯Java开发模型,告别复杂胶水代码:JNI需要开发者编写繁琐的C/C++桥接代码和头文件,维护多套工具链,工作量大且容易出错。而FFM API允许开发者在纯Java代码中直接描述和调用本地函数、操作本地内存,省去了所有中间步骤。
更高的安全性:JNI中,一个错误的指针操作就可能导致JVM崩溃。FFM API则对内存的分配、访问和释放提供了更严格的类型检查和生命周期管理(通过`MemorySession`),能有效预防悬垂指针、内存泄漏等常见且危险的错误。
更标准、规范的API:JNI因其复杂性,催生了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%。
RMS
1344字节转换为float值,共336组,聚合方法--RMS--信号的(等效直流能力),反映了信号的强度或振幅
RMS归一化(均方根归一化)为[0,1]
八、WebSocket
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端。因此,在WebSocket中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。
旧方案,否:
Java与前端怎么交互?通过 WebSocket 协议,Java是服务器端,浏览器是客户端
准备:
确认JDK版本为JDK 21
- C++ DLL导出函数的头文件
- DLL文件的位置
C++
promise-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.注解--Java
@Override是Java的注解,表示"我要覆盖父类/接口的方法"。
告诉阅读者这是一个重写方法
8.C++调用Java:C++异步工作,需要通知Java事件
9.如果没有C++→Java回调会怎样?
Java只能 轮询 (不断问C++"有没有数据?")
轮询的问题 :
效率低 :大部分时间都在空等
延迟高 :最多延迟100ms才能收到数据
浪费资源 :占用CPU和线程
回调的优势 :
效率高 :只有有数据时才处理
零延迟 :数据到达立即通知
资源节约 :不需要轮询线程
10.JNI回调是"做完某事"还是"触发某个事件"?
两个都是,只是对同一个意思不同描述。
十、JNI基础
1.JNI
JNI(Java 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++线程中获取当前线程的JNIEnv
6.1.1AttachCurrentThread()与DetachCurrentThread()
是一个 JNI 函数,用于将当前 native 线程连接(attach)到 Java 虚拟机(JVM),从而让该线程能够进行 JNI 调用,访问 Java 对象和方法。
AttachCurrentThread:将C++线程附加到Java虚拟机(JVM),获取该线程的JNIEnv*。
AttachCurrentThread()和DetachCurrentThread()必须成对调用,否则会导致线程资源泄漏,甚至 JVM 崩溃。
任何本地线程调用Java方法,都必须先attach获取自己的JNIEnv。
本项目每次回调都attach / detach?
Attach注册线程到JVM,Detach解除注册;仅首次需要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_context
g_jni_context是全局上下文对象,包含了JNI桥接层需要的所有状态和资源。
g_jni_context.control_io_context_ = std::make_unique<boost::asio::io_context>();
control_io_context_是std::make_unique<boost::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_shared
make_unique→ 独占、轻量、无计数,首选
make_shared→ 共享所有权、带引用计数、支持 weak_ptr,连续内存分配更快但内存回收有延迟
8.JNI编译与链接
8.1 DLL编译
8.2 Java头文件生成
9.常见错误与注意
9.1常见错误
9.2注意
使用全局引用保存跨线程使用的Java对象
在非Java线程中调用Java方法时,必须先AttachCurrentThread
使用完局部引用后及时DeleteLocalRef
JNI函数名必须严格按照Java_包名_类名_方法名格式
使用extern "C"导出JNI函数
关闭时按照正确的顺序释放资源
10.为什么要有JNI桥接层
解耦 :AppController不需要知道Java的存在
转换 :处理跨语言数据类型转换
线程安全 :处理跨线程调用问题
生命周期管理 :统一管理资源的创建和释放
11.回调驱动设计
Java不需要轮询,而是等待C++通知
事件驱动:数据到达 → C++回调 → Java处理 → 更新前端
JNI与FFM,JNI缺点,为什么要用FFM
Java线程分类
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 FFM(Foreign 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 世界用来 安全访问这块内存的带类型安全的代理对象 。没有 MemorySegment,Java 无法以类型安全的方式读写原生内存。
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共享同一个Stub
7.MethodHandle与Handle
7.1MethodHandle
作用 :封装原生函数调用,提供类型安全的调用接口。
7.2Handle
C 端通过 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 调用 DLL,Java调用方,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 回调的关键机制。
stub=C接口外壳+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。 原因:样本量变多,更容易采集到罕见的长尾慢请求,长尾效应显现。