在尝试用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