核心概念:Java Native Interface (JNI)
JNI 是 Java 平台的一部分,它定义了一套规范,允许 Java 代码和其他语言(主要是 C/C++)进行交互,它就像一座桥梁,让 Java 可以调用 C/C++ 的函数,反之亦然。

调用流程图:
[Java Code] <---> (JVM - Java Virtual Machine) <---> [JNI Header Files] <---> [C/C++ Code]
^ ^
| |
(调用 `native` 方法) (实现 `native` 方法)
完整步骤与实例
我们将创建一个简单的例子:
- Java 侧: 定义一个
native方法(一个没有实现体的方法)。 - 编译 Java 代码: 生成
.class文件。 - 生成 C 头文件: 使用
javah(旧版) 或javac -h(新版) 工具,根据.class文件生成 C 语言头文件 (.h),这个头文件包含了 Javanative方法的 C 函数声明。 - 编写 C 代码: 实现头文件中声明的 C 函数。
- 编译 C 代码: 将 C 代码编译成共享库(
.so文件),在 Linux 下,共享库就是 JNI 的“胶水”。 - 运行 Java 程序: 告诉 JVM 在哪里找到我们编译好的
.so文件,并执行 Java 代码。
详细步骤
第 1 步:准备 Java 代码
创建一个名为 HelloJNI.java 的文件,注意 native 关键字,它表示这个方法的实现在其他地方(这里是 C 语言)。
HelloJNI.java

public class HelloJNI {
// 声明一个 native 方法
public native void sayHello();
// static 代码块在类被加载时执行,通常用于加载本地库
static {
// System.loadLibrary 会加载名为 "hellojni" 的共享库 (libhellojni.so)
// 它会按照标准路径查找,如 LD_LIBRARY_PATH
System.loadLibrary("hellojni");
}
public static void main(String[] args) {
new HelloJNI().sayHello();
}
}
第 2 步:编译 Java 代码
打开终端,进入 HelloJNI.java 所在的目录,执行编译命令:
# javac 是 Java 编译器 javac HelloJNI.java
执行后,会生成一个 HelloJNI.class 文件。
第 3 步:生成 C 头文件
这是连接 Java 和 C 的关键一步,我们需要根据 HelloJNI.class 文件生成一个 C 语言头文件。
重要提示: 从 JDK 10 开始,javah 命令已被废弃,推荐使用 javac -h 选项。
# javac -h . HelloJNI.class
-h .: 指定头文件生成的目录, 表示当前目录。HelloJNI.class: 要处理的 Java 类文件。
执行后,会在当前目录生成一个名为 HelloJNI.h 的头文件。不要手动修改这个文件!
HelloJNI.h (内容示例)
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
关键点解读:
JNIEXPORT,JNICALL: JNI 定义的宏,用于标识函数的导出和调用规范。Java_HelloJNI_sayHello: 这是 C 函数的命名规则,非常严格:Java_+包名(如果有)_+类名+_+方法名。(JNIEnv *, jobject): 这是函数的参数。JNIEnv *: 指向 JNI 环境的指针,通过它可以在 C 代码中调用 Java 的功能。jobject: 指向调用该方法的 Java 对象的引用(如果是static方法,则是jclass,指向类对象)。
void: 返回值类型,对应 Java 中的void。()V: 这是方法的签名,对应 Java 中的 (无参数) 和V(void 返回值),签名规则很重要,稍后会提到。
第 4 步:编写 C 代码
创建一个 HelloJNI.c 文件,实现 HelloJNI.h 中声明的函数。
HelloJNI.c
#include <stdio.h>
#include "HelloJNI.h" // 包含生成的头文件
// 实现 Java_HelloJNI_sayHello 函数
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
printf("Hello from C! This is printed by the C code.\n");
// (可选) 通过 JNIEnv 调用 Java 方法
// 调用 System.out.println
// jclass clazz = (*env)->FindClass(env, "java/lang/System");
// jfieldID outID = (*env)->GetStaticFieldID(env, clazz, "out", "Ljava/io/PrintStream;");
// jobject outObj = (*env)->GetStaticObjectField(env, clazz, outID);
// jclass printStreamClass = (*env)->FindClass(env, "java/io/PrintStream");
// jmethodID printlnID = (*env)->GetMethodID(env, printStreamClass, "println", "(Ljava/lang/String;)V");
// jstring message = (*env)->NewStringUTF(env, "Hello from C via Java call!");
// (*env)->CallVoidMethod(env, outObj, printlnID, message);
}
- 我们先实现最简单的功能:直接在 C 代码中打印一句话。
JNIEnv *env的使用方式在 C 和 C++ 中略有不同,上面的代码是 C 语言的写法,如果是 C++,JNIEnv是一个类的实例,可以直接调用方法,如env->FindClass(...)。
第 5 步:编译 C 代码为共享库 (.so 文件)
这是在 Linux 下最关键的一步,我们需要使用 GCC (GNU Compiler Collection) 将 C 源文件编译成动态链接库(Shared Object, .so)。
# gcc -fPIC -shared -I ${JAVA_HOME}/include -I ${JAVA_HOME}/include/linux -o libhellojni.so HelloJNI.c
命令分解:
gcc: GNU C 编译器。-fPIC: (Position-Independent Code) 生成位置无关代码,这是创建共享库的必要选项。-shared: 生成共享库(.so文件)而不是可执行文件。-I <path>: 指定头文件搜索路径,我们需要包含 JNI 的头文件。${JAVA_HOME}/include: 包含核心 JNI 头文件,如jni.h。${JAVA_HOME}/include/linux: 包含 Linux 平台特定的 JNI 头文件。- 注意: 请确保
${JAVA_HOME}环境变量已正确设置为你 JDK 的安装路径。
-o libhellojni.so: 指定输出的共享库名称。Java 的System.loadLibrary("hellojni")会查找lib<name>.so格式的文件,所以我们命名输出为libhellojni.so。HelloJNI.c: 我们的 C 源文件。
执行成功后,你会得到一个 libhellojni.so 文件。
第 6 步:运行 Java 程序
Java 代码、C 代码和它们之间的“桥梁”(.so 文件)都准备好了,让我们运行它。
System.loadLibrary("hellojni") 默认会在系统库路径(如 /lib, /usr/lib)中查找 libhellojni.so,但通常我们把它放在当前目录,所以需要通过 LD_LIBRARY_PATH 环境变量来告诉去哪里找。
# 设置 LD_LIBRARY_PATH 为当前目录 (.) export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH # 运行 Java 程序 # java -cp . HelloJNI
或者,用一行命令完成:
# java -Djava.library.path=. -cp . HelloJNI
-Djava.library.path=.: 这个 JVM 参数可以指定加载本地库的路径,它会覆盖LD_LIBRARY_PATH,这里我们指定为当前目录 。
预期输出:
Hello from C! This is printed by the C code.
恭喜!你已经成功地在 Java 中调用了 C 代码!
进阶主题与注意事项
数据类型映射
Java 和 C 的数据类型不能直接混用,需要通过 JNI 进行转换。
| Java 类型 | C 类型 | 描述 |
|---|---|---|
byte |
jbyte |
8-bit 有符号整数 |
short |
jshort |
16-bit 有符号整数 |
int |
jint |
32-bit 有符号整数 |
long |
jlong |
64-bit 有符号整数 |
float |
jfloat |
32-bit 浮点数 |
double |
jdouble |
64-bit 浮点数 |
char |
jchar |
16-bit Unicode 字符 |
boolean |
jboolean |
JNI_TRUE (1) 或 JNI_FALSE (0) |
Object |
jobject |
任何 Java 对象 |
Class |
jclass |
Java 类对象 |
String |
jstring |
Java 字符串 |
Object[] |
jobjectArray |
任意对象的数组 |
byte[] |
jbyteArray |
字节数组 |
| ... | ... | ... |
方法签名
当 Java 方法的参数或返回值不是基本类型时,需要使用签名来描述。
()V: 无参数,返回 void(I)I: 一个int参数,返回int(Ljava/lang/String;)V: 一个String参数,返回void([I)[I: 一个int数组参数,返回int数组
可以使用 javap 工具来查看类的签名:
javap -s -p HelloJNI.class
更好的替代方案
虽然 JNI 功能强大,但它也有一些缺点:
- 复杂: 手动管理内存、类型转换,容易出错。
- 破坏 Java 的跨平台性: 为每个平台(Windows, Linux, macOS)都需要编译不同的本地库。
- 性能开销: Java 和 C 之间的数据传递有性能损耗。
在考虑使用 JNI 之前,请评估是否有更好的替代方案:
-
JNA (Java Native Access):
- 优点: 比 JNI 简单得多!它通过一个简单的接口描述文件(或注解)让你直接在 Java 中调用 C 函数,无需编写任何 C 代码或处理复杂的类型转换,JNA 在后台帮你处理了所有 JNI 的繁琐工作。
- 缺点: 可能有轻微的性能开销(通常可以忽略不计)。
- 推荐: 对于大多数应用,JNA 是比 JNI 更好的选择。
-
GraalVM Native Image:
- 优点: 这是一个革命性的技术,它可以将 Java 应用程序(包括其依赖项)编译成一个独立的、本地的可执行文件,启动速度极快,内存占用极低,你可以将 C/C++ 代码(通过
JNI或Foreign Function & Memory API)直接编译进这个原生镜像中。 - 缺点: 需要改变构建流程,不支持所有 Java 特性(如动态类加载)。
- 优点: 这是一个革命性的技术,它可以将 Java 应用程序(包括其依赖项)编译成一个独立的、本地的可执行文件,启动速度极快,内存占用极低,你可以将 C/C++ 代码(通过
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JNI | - 功能最强大,直接与 JVM 交互 - 性能最高(数据传递后) |
- 非常复杂,繁琐 - 需要编写和维护 C/C++ 代码 - 破坏跨平台性 |
- 极致性能要求的模块 - 需要深度 JVM 内部交互 - 复杂的 C/C++ 库集成 |
| JNA | - 非常简单,只需 Java 代码 - 自动处理类型转换和内存管理 - 跨平台 |
- 可能有轻微性能开销 - 对复杂 C 结构体支持有限 |
- 快速集成现有 C/C++ 库 - 大多数常见的本地代码调用场景 |
| GraalVM | - 极致的启动速度和低内存 - 真正的原生体验 |
- 改变构建和部署模式 - 不支持所有 Java 特性 |
- 微服务、Serverless、命令行工具 - 需要最小化资源占用的应用 |
对于新项目,除非你有非常特殊的需求(需要编写新的、高度优化的本地算法并直接操作 JVM 内部数据),否则强烈建议优先考虑 JNA,如果你只是想学习底层原理或者维护一个古老的 JNI 项目,那么掌握 JNI 仍然是必要的。
