下面我将分步详细解释整个过程,从环境准备到具体编码,再到编译和运行。

核心思想
整个过程可以概括为以下几个步骤:
- 编写 Java 代码:创建一个 Java 类,其中包含你想要从 C 代码调用的方法,这些方法需要使用
native关键字声明,表示它们将由本地代码(C/C++)实现。 - 生成 C 头文件:使用 Java 自带的
javac编译 Java 代码,然后用javah(在 JDK 8 及之前)或javac -h(在 JDK 9 及之后)工具,根据编译后的.class文件生成一个 C/C++ 头文件(.h),这个头文件包含了 C 函数的声明,这些函数的名称和签名是 JNI 规定的。 - 编写 C 代码:创建一个 C 源文件(
.c),实现步骤 2 中生成的头文件里声明的函数,在这个 C 函数内部,你可以使用 JNI 提供的 API 来创建 Java 对象、调用 Java 方法、访问 Java 字段等。 - 编译链接:使用 GCC 或 Clang 等 C 编译器,将你的 C 源文件编译成一个动态共享库(
.so文件),在编译时,需要链接 JNI 的库。 - 运行:在 Linux 终端中运行你的 C 程序,C 程序会加载 Java 虚拟机(JVM),通过 JNI 调用 Java 代码,并处理返回结果。
详细步骤与示例
让我们通过一个完整的例子来走一遍流程。
第 1 步:准备环境
确保你的系统已经安装了 JDK,打开终端,检查版本:
java -version javac -version
你需要 java (运行时) 和 javac (编译器) 两个工具。

第 2 步:编写 Java 代码
创建一个名为 JNITest.java 的文件,这个类将包含一个 native 方法。
JNITest.java
public class JNITest {
// 这是一个本地方法,由 C 语言实现
public native void sayHello();
// 一个普通方法,供 C 代码调用
public void printMessage(String message) {
System.out.println("Java 收到消息: " + message);
}
// 一个静态方法,供 C 代码调用
public static int add(int a, int b) {
System.out.println("Java 执行加法: " + a + " + " + b);
return a + b;
}
public static void main(String[] args) {
System.out.println("Hello from Java!");
}
}
第 3 步:编译 Java 代码并生成 C 头文件
-
编译
.java文件:javac JNITest.java
这会生成
JNITest.class文件。
(图片来源网络,侵删) -
生成 C 头文件:
- 对于 JDK 9 及以上版本(推荐):
javac -h . JNITest.java
这会在当前目录下生成一个
JNITest.h头文件。 - 对于 JDK 8 及以下版本:
javac JNITest.java javah JNITest
这会生成一个
com_example_JNITest.h(包名会体现在路径中)或JNITest.h文件。
- 对于 JDK 9 及以上版本(推荐):
生成的 JNITest.h 文件内容(简化后):
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNITest */
#ifndef _Included_JNITest
#define _Included_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNITest
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNITest_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
注意函数名 Java_JNITest_sayHello,这就是 JNI 规定的命名规则:Java_ + 包名(如果有) + 类名 + 方法名。
第 4 步:编写 C 代码
创建一个 jni_impl.c 文件来实现 JNITest.h 中声明的函数。
jni_impl.c
#include <stdio.h>
#include <jni.h> // 必须包含 JNI 头文件
#include "JNITest.h" // 包含我们生成的头文件
// 实现 Java_JNITest_sayHello 函数
JNIEXPORT void JNICALL Java_JNITest_sayHello(JNIEnv *env, jobject obj) {
// 1. 获取 JNITest 类的引用
jclass clazz = (*env)->GetObjectClass(env, obj);
// 2. 获取 printMessage 方法的 ID
// 方法名: "printMessage"
// 描述符: "(Ljava/lang/String;)V" 表示接收一个 String 参数,返回 void
jmethodID mid_print = (*env)->GetMethodID(env, clazz, "printMessage", "(Ljava/lang/String;)V");
// 3. 创建一个 Java 字符串对象
jstring message = (*env)->NewStringUTF(env, "你好,来自 C 世界的问候!");
// 4. 调用 Java 对象的 printMessage 方法
(*env)->CallVoidMethod(env, obj, mid_print, message);
// 5. 释放局部引用(防止内存泄漏)
(*env)->DeleteLocalRef(env, message);
(*env)->DeleteLocalRef(env, clazz);
// --- 调用静态方法 add 的示例 ---
printf("--- 调用静态方法 add ---\n");
// 1. 获取 JNITest 类的引用
jclass clazz_static = (*env)->FindClass(env, "JNITest");
// 2. 获取静态方法 add 的 ID
// 描述符: "(II)I" 表示接收两个 int 参数,返回一个 int
jmethodID mid_add = (*env)->GetStaticMethodID(env, clazz_static, "add", "(II)I");
// 3. 调用静态方法
jint result = (*env)->CallStaticIntMethod(env, clazz_static, mid_add, 10, 20);
printf("C 收到 Java add 方法的返回值: %d\n", result);
// 4. 释放局部引用
(*env)->DeleteLocalRef(env, clazz_static);
}
关键点解释:
JNIEnv *env:这是 JNI 的核心,是一个指向 JNI 环境的指针,所有 JNI 函数的第一个参数都是它。jobject obj:对于实例方法,这个参数代表调用该方法的 Java 对象本身(this),对于静态方法,它会是NULL。(*env)->...:在 C 语言中,JNIEnv是一个指针的指针,所以需要这样调用函数,在 C++ 中,JNIEnv是一个类对象,可以直接调用env->...。- 方法描述符:非常重要,它定义了方法的参数和返回类型。
()V:无参数,返回 void。(I)I:一个 int 参数,返回 int。(Ljava/lang/String;)V:一个java.lang.String参数,返回 void。([B)[B:一个 byte 数组参数,返回 byte 数组。- 可以使用
javap -s命令查看类的描述符:javap -s JNITest
第 5 步:编译链接生成动态库
使用 GCC 编译 jni_impl.c,生成 libjnitest.so 文件。
gcc -shared -fpic -o libjnitest.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux jni_impl.c -L${JAVA_HOME}/jre/lib/amd64/server -ljvm
命令参数解释:
-shared:生成一个共享库(.so文件)。-fpic:生成位置无关代码,这是共享库的要求。-o libjnitest.so:指定输出的库文件名。注意:库文件名通常以lib开头。-I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux:指定 JNI 头文件的搜索路径。${JAVA_HOME}是你 JDK 的安装目录,/usr/lib/jvm/java-11-openjdk-amd64。-L${JAVA_HOME}/jre/lib/amd64/server:指定 JVM 库的搜索路径。-ljvm:链接 JVM 库,这是 JNI 调用的基础。
注意:${JAVA_HOME}/jre/lib/amd64/server 这个路径在不同 JDK 版本和架构下可能不同,请根据你的实际情况调整,你也可以用 find /usr/lib/jvm -name "libjvm.so" 来找到它的确切位置。
第 6 步:编写 C 程序入口并运行
我们需要一个 C 程序来启动 JVM 并加载我们刚刚创建的库。
main.c
#include <jni.h>
#include <stdio.h>
// JNI_OnLoad 函数会在库被加载时由 JVM 自动调用
// 我们可以在这里做一些初始化工作,但本例中不需要
int main(int argc, char **argv) {
JavaVM *jvm; // JVM 实例
JNIEnv *env; // JNI 环境指针
jclass cls;
jmethodID mid;
// 1. 初始化 JVM 参数
JavaVMInitArgs vm_args;
JavaVMOption options[1];
// 设置 JVM 的类路径,确保能找到 JNITest.class
// "." 表示当前目录
options[0].optionString = "-Djava.class.path=.";
vm_args.version = JNI_VERSION_1_8; // 使用你需要的 JNI 版本
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = 0;
// 2. 创建 JVM
jint res = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
if (res != JNI_OK) {
fprintf(stderr, "Cannot create Java VM\n");
return -1;
}
// 3. 获取 JNITest 类
cls = (*env)->FindClass(env, "JNITest");
if (cls == NULL) {
fprintf(stderr, "Cannot find JNITest class\n");
(*jvm)->DestroyJavaVM(jvm);
return -1;
}
// 4. 获取 sayHello 方法的 ID
mid = (*env)->GetMethodID(env, cls, "sayHello", "()V");
if (mid == NULL) {
fprintf(stderr, "Cannot get Method ID for sayHello\n");
(*jvm)->DestroyJavaVM(jvm);
return -1;
}
// 5. 创建 JNITest 对象实例
jobject obj = (*env)->NewObject(env, cls, mid);
if (obj == NULL) {
fprintf(stderr, "Cannot create JNITest object\n");
(*jvm)->DestroyJavaVM(jvm);
return -1;
}
// 6. 调用 sayHello 方法
printf("C 程序准备调用 Java 的 sayHello 方法...\n");
(*env)->CallVoidMethod(env, obj, mid);
printf("C 程序调用 Java 方法完毕,\n");
// 7. 销毁 JVM
(*jvm)->DestroyJavaVM(jvm);
return 0;
}
编译并运行主程序:
-
编译
main.c:gcc -o my_c_app main.c -L. -ljnistest
-o my_c_app:输出的可执行文件名为my_c_app。-L.:告诉编译器在当前目录()下查找库文件。-ljnistest:链接名为jnitest的库,编译器会自动在前面加上lib,并加上.so后缀,去寻找libjnitest.so。
-
运行
my_c_app:./my_c_app
如果运行时出现
libjnitest.so: cannot open shared object file错误,是因为系统找不到你刚刚生成的.so文件,你有两种方法解决:- 方法一(推荐):将
.so文件复制到标准库路径,如/usr/lib。sudo cp libjnitest.so /usr/lib
- 方法二(临时):设置
LD_LIBRARY_PATH环境变量。export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./my_c_app
- 方法一(推荐):将
预期输出:
C 程序准备调用 Java 的 sayHello 方法...
Hello from Java!
Java 收到消息: 你好,来自 C 世界的问候!
--- 调用静态方法 add ---
Java 执行加法: 10 + 20
C 收到 Java add 方法的返回值: 30
C 程序调用 Java 方法完毕。
总结与注意事项
- 性能:JNI 调用有性能开销,因为它涉及本地代码和 JVM 之间的上下文切换和数据转换,不要在性能敏感的循环中进行频繁的 JNI 调用。
- 内存管理:在 JNI 中创建的引用(如
jobject,jclass,jstring)分为局部引用和全局引用,局部引用只在当前线程的 JNI 方法调用期间有效,方法返回后会被自动释放,如果你想在多次 JNI 调用之间保持一个 Java 对象的引用,需要将其转换为全局引用(NewGlobalRef),并在用完后手动释放(DeleteGlobalRef),忘记释放会导致内存泄漏。 - 异常:JNI 调用可能会抛出 Java 异常,C 代码需要检查异常是否发生(
ExceptionCheck),如果发生,必须处理(ExceptionDescribe打印异常信息,ExceptionClear清除异常),否则后续的 JNI 调用会失败。 - 线程:默认情况下,一个 JVM 只能被一个线程附加,如果你想在 C 程序的多个线程中使用 JNI,每个线程都需要通过
AttachCurrentThread附加到 JVM,并在结束时用DetachCurrentThread分离。 - 复杂性:JNI 本身很复杂,特别是当处理复杂数据类型(如数组、对象)时,对于更复杂的交互,可以考虑使用 JNA (Java Native Access) 或 Swig 等工具,它们可以简化本地代码的编写和绑定过程。
