在尝试用Java实现一个调节系统音量的小工具时,我感到深深的无力。只得求助于更贴近底层的C语言,用JNI来实现。以此为契机,简单总结一下JNI。
什么是JNI JNI(Java Native Interface)是Java提供的一套编程接口,用于实现Java代码与本地代码的交互。它允许Java程序调用本地代码,也允许本地代码回调Java代码。
为什么要用JNI
由于Java无法直接访问硬件、操作系统、数据库等底层资源,因此需要借助c/c++来实现。
对于一些已有的用其它编程语言实现的库,程序员可以直接使用,无需重新实现。
使用底层的库,如计算、图形、渲染,往往可以获得更好的性能。
如何应用JNI 因为网上有很多相关的资料,就不再赘述了。简言之:
在Java类中使用native关键字声明一个方法。
使用 javac -h 命令生成头文件。
创建一个c/c++文件,引入头文件,并实现对应的方法。
使用本地编译器(如gcc)编译c/c++文件,生成动态链接库。
在Java代码中引入动态链接库,并调用对应的方法。
按照以上顺序,就能实现一个简单的JNI程序。
JNI的深层理解 我们知道无论什么语言,最终都需要编译成机器码,存入到内存中,再由CPU读取运行。所以在CPU层面上看,JNI调用的本质就是一段机器码(由Java JIT生成)调用了另一段机器码(由c编译器生成),只是一个简单的函数跳转。
此处可以看出,JNI的调用并不是像微服务架构那样,将Java和本地代码分别开多个进程,进程间通信(IPC)。而是在Java进程的堆区中额外划分出C本地堆(C Native Heap),在使用malloc()、new 或者JNI函数(如 GetStringUTFChars)时,所申请的内存就来自此处,通过函数指针进行的高效内存调用。这使得JNI相比于微服务架构,天然的有更低的资源开销和更快的性能。
对于一段由c/c++/Rust等语言编写的本地代码,在被Java调用前,就需要由对应的编译器(如gcc,clang,rustc)直接编译为针对特定操作系统和CPU架构的机器码,并打包在动态链接库中,这也就是上文中第四步做的事。
当Java代码执行到 native 方法时,会发生以下几步:
JVM已经通过JIT编译器,将调用这个 native 方法的Java代码本身编译成了机器码。
这段机器码的执行逻辑是:“准备参数,然后调用 Java_Class_Method 这个函数”。
JVM在之前加载动态库时,已经将 Java_Class_Method 这个符号解析为C代码编译后所在的内存地址。
CPU的执行流于是直接跳转到动态库中那块预先编译好的C机器码的地址,开始执行。
C代码执行完毕后,再通过返回指令跳转回JIT编译好的Java机器码中继续执行。
按照这个逻辑理解,是否只要最终能编译成机器码,就可以交互呢?答案是否定的,至少对于Java来说。
从JNI的应用角度看,在第五步引入动态链接库后,还有两大关键问题需要解决:
如何以一种稳定、普适、高效的方式,在二进制层面与各种本地代码库进行连接和基本通信的问题。
如何安全地交换数据(协调GC)、保持类型安全、处理异常等具体业务问题。
我们一个一个来看。
问题一 在解决这个问题前,先回到问题本身,我们可能会好奇于,都编译成机器码了还需要额外的规范来协调吗?难道机器码还有区别吗?
当然,机器码本身没有不同,但调用函数时如何准备参数、如何传递、如何返回,不同的约定会导致完全不同的机器码排列组合。如果不遵守同一套约定,双方就无法正确对话。
举个例子来理解,假设我们的程序由两部分组成:主程序 main.exe 和库 library.dll,并有一个简单的函数add。
1 int result = add(10, 20);
现在,main.exe 调用 library.dll 中的 add 函数:
main.exe 按照自己的规则,生成机器码:先压入 20,再压入 10,然后调用 add。
add 函数开始执行,它按照自己的规则从栈上读取参数。它以为第一个参数在最左边,于是读到了 10(但实际上应该是 20),第二个参数读到了 20(但实际上应该是 10)。虽然结果是 30,但过程是错的。
函数返回后,add 函数按照自己的规则,清理了栈上的两个参数。
执行权回到 main.exe。main.exe 也按照自己的规则,又一次尝试清理栈上的两个参数。
此时,栈指针已经被彻底破坏,完全错位。程序几乎必然会在后续的操作中崩溃。
由此看出,尽管双方生成的机器码本身都是合法、正确的,但因为遵循了不同的“调用约定”,导致程序错误。
JNI最终采用C ABI(C语言应用程序二进制接口)作为双方的约定,这不仅仅是因为它的稳定和兼容性,更是因为JVM本身就是由C/C++编写的应用程序。可以说C ABI是JVM的“母语”,用它来与外界沟通是效率最高、最可靠的方式。
问题二 由于JVM机制的特殊性,与本地代码存在着两大根本的冲突:
为了优化性能(减少内存碎片),GC(垃圾回收)会定期移动内存中的对象。这就导致C/C++无法获得一个可靠的对象地址,一旦GC移动对象,C指针立即变成悬空指针,导致程序崩溃。
Java的强类型和沙箱机制,不会允许你访问一个对象的私有字段。而C/C++可以访问任意内存,没有内置的安全检查,这就导致数据泄露和安全漏洞。
这就需要JNI的特定规范来约束本地代码,让他们以一种JVM能够理解和安全管理的方式运行。具体来讲,一方面JNI禁止直接操作指针,而是引入了 “JNI引用”(jobject, jstring等) 的概念,使JVM能在背后协调本地代码对对象的访问:要么临时“钉住”对象不让GC移动(Pin),要么先复制一份数据(Copy);另一方面,JNI强制所有访问都必须通过 JNIEnv* 提供的函数(如 GetObjectField, GetIntArrayElements),在执行操作前进行必要的安全检查(如检查数组索引是否越界)。
此处的Pin和Copy策略的选择完全取决于JVM,虽然不同的策略会有所差异(如对应的内存地址不同),但作为Java开发者,无法也不应该假设JVM会采用哪种策略,而是必须按照规范编写代码(即总是成对调用Get/Release),以保证在任何情况下都是正确的。
最终,C ABI提供了基础的、跨平台的函数调用机制,而JNI特定规范则在此基础上定义了专属于JVM的、安全的内存对象交互语义。两者结合,共同构成了Java与本地代码交互的完整解决方案。
JNI的内存管理 到上文为止,Java与本地代码的交互机制已经比较完整了。但是还有一些细节没有涉及,如明明在同一片内存中,为什么垃圾回收机制不会影响到C/C++呢?
当然你可能会觉得为什么不让垃圾回收机制统一管理本地代码呢,这样不是更方便吗?这不是技术上无法实现,而是有其巧思在此,主要是为了兼顾性能、灵活性和语言特性而必须付出的代价。此处不深究。
这个问题可以从内存的角度来看,虽然Java代码和本地代码都运行在整个Java进程的内存上,但其内部还有划分。在Java进程的堆区中会额外划分出C本地堆(C Native Heap),在使用malloc()、new 或者JNI函数(如 GetStringUTFChars)申请的内存就来自此处,而JVM的垃圾回收器(GC)完全不关心这块区域。申请了就必须释放,否则就会泄漏。而两者的的栈帧则是共用同一个进程栈,这也是为什么C函数中的局部参数会自动回收。
JNA和JNR JNA (Java Native Access),是一个开源库,它基于JNI,但提供了一个纯Java的接口来访问本地库,不需要写任何C/C++代码。
示例:调用 C 标准库函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import com.sun.jna.Library;import com.sun.jna.Native;import com.sun.jna.Platform;import com.sun.jna.Pointer;import com.sun.jna.Structure;import com.sun.jna.ptr.IntByReference;public interface CLibrary extends Library { CLibrary INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c" , CLibrary.class); int printf (String format, Object... args) ; long strlen (String str) ; long time (Pointer pointer) ; String getenv (String name) ; int gettimeofday (TimeVal tv, Pointer tz) ; } @Structure .FieldOrder({"tv_sec" , "tv_usec" })public class TimeVal extends Structure { public static class ByReference extends TimeVal implements Structure .ByReference {} public long tv_sec; public long tv_usec; @Override public String toString () { return tv_sec + "秒 " + tv_usec + "微秒" ; } } public class JnaExample { public static void main (String[] args) { CLibrary.INSTANCE.printf("Hello, World from JNA!\\n" ); String testStr = "Hello JNA" ; long length = CLibrary.INSTANCE.strlen(testStr); System.out.println("字符串 '" + testStr + "' 的长度是: " + length); long currentTime = CLibrary.INSTANCE.time(null ); System.out.println("当前时间戳: " + currentTime); String path = CLibrary.INSTANCE.getenv("PATH" ); System.out.println("PATH环境变量: " + (path != null ? path : "未找到" )); TimeVal timeVal = new TimeVal (); CLibrary.INSTANCE.gettimeofday(timeVal, null ); System.out.println("当前时间: " + timeVal); IntByReference ref = new IntByReference (42 ); System.out.println("IntByReference值: " + ref.getValue()); } }
高级特性:调用自定义库
假设我们有一个自定义的 C 库 mylib,其中包含以下函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #ifndef MYLIB_H #define MYLIB_H typedef struct { int x; int y; } Point; int add (int a, int b) ;double multiply (double a, double b) ;void modify_point (Point* p) ;#endif
对应的 JNA 调用代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import com.sun.jna.Library;import com.sun.jna.Native;import com.sun.jna.Structure;@Structure .FieldOrder({"x" , "y" })class Point extends Structure { public int x; public int y; public Point () {} public Point (int x, int y) { this .x = x; this .y = y; } } interface MyLib extends Library { MyLib INSTANCE = Native.load("mylib" , MyLib.class); int add (int a, int b) ; double multiply (double a, double b) ; void modify_point (Point p) ; } public class CustomLibExample { public static void main (String[] args) { int sum = MyLib.INSTANCE.add(5 , 3 ); System.out.println("5 + 3 = " + sum); double product = MyLib.INSTANCE.multiply(2.5 , 4.0 ); System.out.println("2.5 * 4.0 = " + product); Point p = new Point (10 , 20 ); System.out.println("修改前: (" + p.x + ", " + p.y + ")" ); MyLib.INSTANCE.modify_point(p); System.out.println("修改后: (" + p.x + ", " + p.y + ")" ); } }
JNR (Java Native Runtime),由JRuby团队开发,旨在解决JNA的性能问题。它同样允许纯Java代码调用本地库。JNR的设计比JNA更底层、更智能。它的核心是 libffi(一个广泛使用的外部函数接口库)。
示例:调用 C 标准库函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import jnr.ffi.LibraryLoader;import jnr.ffi.Pointer;import jnr.ffi.annotations.Delegate;import jnr.ffi.annotations.In;import jnr.ffi.annotations.Out;import jnr.ffi.annotations.Transient;import jnr.ffi.types.size_t;import java.nio.ByteBuffer;public class JnrExample { public interface LibC { static LibC load () { return LibraryLoader.create(LibC.class).load("c" ); } int printf (String format, Object... args) ; @size_t long strlen (String str) ; long time (Pointer pointer) ; String getenv (String name) ; int gettimeofday (TimeVal tv, Pointer tz) ; } public static class TimeVal extends jnr .ffi.Struct { public final Signed64 tv_sec = new Signed64 (); public final Signed64 tv_usec = new Signed64 (); public TimeVal (jnr.ffi.Runtime runtime) { super (runtime); } } public static void main (String[] args) { LibC libc = LibC.load(); libc.printf("Hello, World from JNR!\\n" ); String testStr = "Hello JNR" ; long length = libc.strlen(testStr); System.out.println("字符串 '" + testStr + "' 的长度是: " + length); long currentTime = libc.time(null ); System.out.println("当前时间戳: " + currentTime); String path = libc.getenv("PATH" ); System.out.println("PATH环境变量: " + (path != null ? path : "未找到" )); TimeVal tv = new TimeVal (jnr.ffi.Runtime.getSystemRuntime()); libc.gettimeofday(tv, null ); System.out.println("当前时间: " + tv.tv_sec.get() + "秒 " + tv.tv_usec.get() + "微秒" ); } }
高级特性:调用自定义库
使用 JNR 调用前面提到的自定义库 mylib:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import jnr.ffi.LibraryLoader;import jnr.ffi.Struct;public class JnrCustomLibExample { public interface MyLib { static MyLib load () { return LibraryLoader.create(MyLib.class).load("mylib" ); } int add (int a, int b) ; double multiply (double a, double b) ; void modify_point (Point p) ; } public static class Point extends Struct { public final Signed32 x = new Signed32 (); public final Signed32 y = new Signed32 (); public Point (jnr.ffi.Runtime runtime) { super (runtime); } } public static void main (String[] args) { MyLib mylib = MyLib.load(); int sum = mylib.add(5 , 3 ); System.out.println("5 + 3 = " + sum); double product = mylib.multiply(2.5 , 4.0 ); System.out.println("2.5 * 4.0 = " + product); Point p = new Point (jnr.ffi.Runtime.getSystemRuntime()); p.x.set(10 ); p.y.set(20 ); System.out.println("修改前: (" + p.x.get() + ", " + p.y.get() + ")" ); mylib.modify_point(p); System.out.println("修改后: (" + p.x.get() + ", " + p.y.get() + ")" ); } }
总的来说,两者都是 JNI 的优秀替代方案,可以大大简化 Java 与本地代码的交互过程。
选择哪一个取决于具体需求:
如果需要快速实现功能并且对性能要求不是极高,选择 JNA
如果需要更好的性能并且愿意学习更复杂的 API,选择 JNR