Visual Studio 2019 JNI实战:C++ DLL开发与Java调用全流程指南

发布时间:2026/7/12 5:22:30
Visual Studio 2019 JNI实战:C++ DLL开发与Java调用全流程指南 1. 项目概述为什么要在C里调用Java如果你是一个长期在Windows平台上用Visual Studio搞C开发的工程师突然有一天产品经理跑过来跟你说“咱们这个高性能计算模块得让后端Java服务能直接调用”或者“需要复用团队现有的、用Java写的一大堆业务逻辑库”你该怎么办直接重写时间成本太高。这时候JNIJava Native Interface就成了连接这两个世界的桥梁。它允许你用C或C编写本地方法然后在Java代码中像调用普通方法一样去调用它们反之亦然。我最近就接手了这么一个任务将一个核心的、对延迟极其敏感的C数据处理模块封装成Java服务可调用的形式。整个开发、调试到最终集成都是在Visual Studio 2019这个“老伙计”里完成的。网上关于JNI的教程不少但很多要么是纯命令行操作要么环境交代不清特别是针对VS2019这个目前企业开发的主流IDE能把环境配通、把调试讲透的实战指南并不多。很多人卡在第一步的环境配置或者被运行时各种诡异的UnsatisfiedLinkError和内存访问冲突搞得焦头烂额。这篇文章我就结合这次实战把从零开始在VS2019中创建C DLL并通过JNI供Java调用的完整流程、核心原理、以及那些只有踩过坑才知道的调试技巧给你掰开揉碎了讲清楚。2. 环境准备与项目骨架搭建在动手写代码之前把环境理顺是成功的一半。JNI开发涉及两套生态Java的编译运行环境和Windows下的C编译环境。两者必须对齐否则后患无穷。2.1 工具链的精确匹配首先明确你的Java版本。打开命令行输入java -version。假设你看到的是java version 17.0.8。记住这个版本号尤其是主版本号“17”。接着找到你的JDK安装目录比如C:\Program Files\Java\jdk-17。在这个目录下有两个关键子目录include: 里面包含了JNI的核心头文件jni.h。include\win32: 在Windows下这里还包含了一个平台相关的头文件jni_md.h。你的C编译器需要能看到这些头文件。同时你还需要找到jvm.lib或jvm.dll的导入库文件它通常位于JDK目录下的lib文件夹或者JRE的bin\server或bin\client目录里。在链接阶段我们需要它。另一方面是Visual Studio 2019。确保你安装了“使用C的桌面开发”工作负载。我们将创建一个动态链接库DLL项目因为这是Java通过JNI加载本地库的标准形式。2.2 在Visual Studio 2019中创建DLL项目打开VS2019选择“创建新项目”搜索并选择“动态链接库(DLL)”给项目起个名字比如NativeCalculator。创建完成后你会得到几个默认文件dllmain.cpp,pch.h,pch.cpp。对于JNI项目我们通常不需要修改dllmain.cpp除非有特殊的DLL入口点需求。我们可以把主要逻辑写在新的源文件里。接下来是关键的一步配置项目属性让VS知道去哪找Java的头文件和库。配置头文件路径在“解决方案资源管理器”中右键项目 - “属性”。确保“配置”是“所有配置”“平台”是“x64”强烈建议使用64位除非你有明确的32位Java环境。进入“C/C” - “常规” - “附加包含目录”。在这里添加你的JDK的include目录和include\win32目录。例如C:\Program Files\Java\jdk-17\include C:\Program Files\Java\jdk-17\include\win32这一步解决了编译时找不到jni.h的问题。配置库目录和链接器进入“链接器” - “常规” - “附加库目录”。添加你的JDKlib目录或者包含jvm.lib的目录。例如C:\Program Files\Java\jdk-17\lib然后进入“链接器” - “输入” - “附加依赖项”。在这里添加jvm.lib。这样链接器就知道在生成DLL时需要链接Java虚拟机库。调整运行时库可选但重要进入“C/C” - “代码生成” - “运行时库”。Java HotSpot VM通常使用/MD或/MDd多线程DLL的运行时库。为了减少潜在冲突建议将你的DLL项目也设置为“多线程DLL (/MD)”Release配置和“多线程调试DLL (/MDd)”Debug配置。这能确保你的DLL和JVM使用相同版本的C运行时库避免内存分配和释放跨不同堆进行。注意这些路径配置最好在“所有配置”下设置一次。但“运行时库”这类设置通常需要分别为Debug和Release配置单独设置。3. JNI核心原理与头文件生成JNI的本质是一套规范它定义了Java虚拟机JVM和本地代码C/C之间的通信协议。理解这个协议是写出健壮代码的基础。3.1 方法签名与数据类型映射Java方法和C函数是通过一个特定的“方法签名”来绑定的。这个签名不仅包含方法名更重要的是参数和返回值的类型描述符。例如一个Java方法public native int add(int a, int b);它的签名是(II)I。括号内是参数类型两个I表示两个int括号外是返回值类型I表示int。JNI为每一种Java基本类型和引用类型都定义了对应的C类型jint,jlong,jfloat,jdouble对应Java的int,long,float,double。jobject对应任何Java对象。jstring对应Java的String对象。jclass对应Java的Class对象。jarray及其子类如jintArray,jobjectArray对应Java数组。在C代码中你需要使用这些JNI类型而不是原生的C类型如int,long。3.2 使用javah或javac -h生成头文件我们不需要手动去编写复杂的C函数声明。Java提供了工具来自动生成。假设我们有一个Java类// 文件com/example/NativeCalculator.java package com.example; public class NativeCalculator { // 声明本地方法 public native int add(int a, int b); public native String greet(String name); public static native void initialize(); }首先编译这个Java文件javac com/example/NativeCalculator.java。对于JDK 8及更早版本使用javah工具javah -jni -o NativeCalculator.h com.example.NativeCalculator这会生成一个NativeCalculator.h头文件。对于JDK 10及以后版本javah已被移除推荐使用javac -hjavac -h ./jni_output com/example/NativeCalculator.java这个命令会在./jni_output目录下生成一个com_example_NativeCalculator.h的头文件。打开生成的头文件你会看到类似这样的内容/* DO NOT EDIT THIS FILE - it is machine generated */ #include jni.h /* Header for class com_example_NativeCalculator */ #ifndef _Included_com_example_NativeCalculator #define _Included_com_example_NativeCalculator #ifdef __cplusplus extern C { #endif /* * Class: com_example_NativeCalculator * Method: add * Signature: (II)I */ JNIEXPORT jint JNICALL Java_com_example_NativeCalculator_add (JNIEnv *, jobject, jint, jint); /* * Class: com_example_NativeCalculator * Method: greet * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_example_NativeCalculator_greet (JNIEnv *, jobject, jstring); /* * Class: com_example_NativeCalculator * Method: initialize * Signature: ()V */ JNIEXPORT void JNICALL Java_com_example_NativeCalculator_initialize (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif注意函数名的生成规则Java_全限定类名点替换为下划线_方法名。对于静态方法如initialize第二个参数是jclass代表调用该方法的类对于实例方法如add,greet第二个参数是jobject代表调用该方法的对象实例。实操心得将这个生成的头文件复制到你的VS2019 C项目中并添加到项目头文件里。然后创建一个新的.cpp源文件例如NativeCalculatorImpl.cpp来实现这些函数。在实现文件的开头#include这个生成的头文件和你自己的预编译头如果用了的话。4. C本地方法的实现与编码细节有了头文件我们就可以在C中实现这些函数了。这是JNI开发的核心也是最容易出错的地方。4.1 基本类型与字符串处理我们来实现add和greet方法。// NativeCalculatorImpl.cpp #include pch.h // 如果使用了预编译头 #include com_example_NativeCalculator.h // 生成的头文件 #include string JNIEXPORT jint JNICALL Java_com_example_NativeCalculator_add (JNIEnv* env, jobject /*obj*/, jint a, jint b) { // 基本类型的处理非常简单直接进行C运算即可。 // 但要注意jint和int在平台上宽度可能不同但在运算中通常可以直接使用。 return a b; } JNIEXPORT jstring JNICALL Java_com_example_NativeCalculator_greet (JNIEnv* env, jobject /*obj*/, jstring jname) { // 1. 将jstring转换为C风格的字符串UTF-8编码。 // 注意GetStringUTFChars可能返回NULL需要检查。 const char* cname env-GetStringUTFChars(jname, nullptr); if (cname nullptr) { return nullptr; // 内存不足时抛出OutOfMemoryError这里直接返回null。 } // 2. 使用C字符串进行业务逻辑处理。 std::string greeting Hello, ; greeting cname; greeting !; // 3. **关键** 释放从JVM获取的字符串资源。 env-ReleaseStringUTFChars(jname, cname); // 4. 将C字符串转换回jstring返回给Java。 return env-NewStringUTF(greeting.c_str()); }关键点解析JNIEnv* env: 这是指向JNI环境的指针所有与JVM交互的操作如转换字符串、创建对象、调用方法都需要通过它提供的函数来完成。在C中我们使用env-FunctionName(...)的语法。GetStringUTFChars/ReleaseStringUTFChars: 这是一对必须成对使用的函数。GetStringUTFChars将Java的Unicode字符串jstring转换为C/C中可以操作的UTF-8编码的const char*。使用完毕后必须调用ReleaseStringUTFChars来释放JVM可能为这个转换分配的内存。忘记释放会导致内存泄漏。NewStringUTF: 将C风格的UTF-8字符串转换回Java的jstring对象。4.2 数组、对象与异常处理处理数组和对象更复杂一些。假设我们要实现一个方法对传入的整型数组每个元素加1。// Java 声明 public native void incrementArray(int[] arr);C实现如下JNIEXPORT void JNICALL Java_com_example_NativeCalculator_incrementArray (JNIEnv* env, jobject obj, jintArray jarr) { // 1. 获取数组指针。模式标志0表示获取指针JNI_COMMIT和JNI_ABORT用于后续提交/放弃修改。 // 这里使用JNI_COMMIT表示获取的指针用于修改并且修改后需要通知JVM。 jint* carr env-GetIntArrayElements(jarr, nullptr); if (carr nullptr) { return; // 内存不足已抛出异常。 } // 2. 获取数组长度。 jsize len env-GetArrayLength(jarr); // 3. 操作数组。 for (jsize i 0; i len; i) { carr[i] 1; // 每个元素加1 } // 4. **关键** 释放数组元素。第三个参数是模式 // 0: 将内容复制回原始Java数组并释放C数组。 // JNI_COMMIT: 将内容复制回Java数组但不释放C数组可继续操作。 // JNI_ABORT: 不复制回Java数组直接释放C数组放弃修改。 env-ReleaseIntArrayElements(jarr, carr, 0); // 5. 异常检查。JNI函数调用可能抛出Java异常但不会中断C执行流。 // 必须在关键操作后检查并做出适当处理如提前返回。 if (env-ExceptionCheck()) { env-ExceptionDescribe(); // 打印异常信息到stderr调试用。 env-ExceptionClear(); // 清除异常防止后续JNI调用崩溃。 // 这里可以选择直接返回或者抛出一个C异常。 return; } }注意事项GetPrimitiveTypeArrayElements/Release...: 和字符串处理类似这也是一对必须成对使用的函数用于在本地代码中直接访问Java数组的内容。它提供了“钉住”pin数组的选项以避免在操作过程中被垃圾回收器移动但更常见的是返回一个副本。无论如何都必须调用Release...。异常处理JNI函数调用如GetStringUTFChars失败可能会在Java端设置一个异常如OutOfMemoryError。但这个异常不会像在Java中那样立即中断执行。C代码会继续运行。因此在调用可能抛出异常的JNI函数后尤其是在完成一系列操作准备返回前必须调用env-ExceptionCheck()来检查是否有待处理的Java异常。如果有通常需要清理本地资源如释放获取的数组或字符串然后立即返回让异常在Java调用栈中向上传播。调用env-ExceptionDescribe()可以在控制台输出异常信息便于调试。4.3 访问Java对象字段与调用方法有时需要在本地代码中读取或修改Java对象的字段甚至调用它的方法。这需要用到“字段ID”和“方法ID”。// Java 类 public class DataHolder { public int value; public void printValue() { System.out.println(Value: value); } } // 对应的本地方法 public native void manipulateObject(DataHolder holder);C实现JNIEXPORT void JNICALL Java_com_example_NativeCalculator_manipulateObject (JNIEnv* env, jobject /*obj*/, jobject jholder) { // 1. 获取对象的类。 jclass clazz env-GetObjectClass(jholder); // 2. 获取字段ID。需要字段名和字段描述符。 // 对于int value描述符是I。 jfieldID fidValue env-GetFieldID(clazz, value, I); if (fidValue nullptr) { return; // 字段未找到已抛出NoSuchFieldError。 } // 3. 获取字段值。 jint currentValue env-GetIntField(jholder, fidValue); // 4. 修改字段值。 env-SetIntField(jholder, fidValue, currentValue * 2); // 5. 获取方法ID。需要方法名和方法签名。 // printValue()的签名是()V。 jmethodID midPrint env-GetMethodID(clazz, printValue, ()V); if (midPrint nullptr) { return; // 方法未找到。 } // 6. 调用实例方法。 env-CallVoidMethod(jholder, midPrint); // 7. 异常检查同上略。 }核心技巧字段ID和方法ID在获取后可以缓存起来避免每次调用都去查找这对性能敏感的场景很重要。通常可以在类初始化时如JNI_OnLoad函数或一个静态本地方法中获取并存储为全局引用jfieldID/jmethodID本身就是指针可以直接缓存但对应的jclass需要做成全局引用。5. 编译、部署与Java端调用C代码写完后在VS2019中按F7编译。确保编译平台如x64与你的Java运行时环境JRE架构一致。编译成功后会在输出目录如x64\Debug\生成NativeCalculator.dll文件。5.1 部署DLLJava程序需要通过System.loadLibrary()或System.load()来加载本地库。System.loadLibrary(“NativeCalculator”): 这个方法会从Java库路径java.library.path系统属性中查找名为NativeCalculator的库。在Windows上它会自动添加前缀“lib”和后缀“.dll”去查找NativeCalculator.dll。你需要确保DLL所在的目录在java.library.path中或者将DLL复制到java.library.path包含的目录之一如当前工作目录、Windows\System32等。System.load(“C:\\full\\path\\to\\NativeCalculator.dll”): 使用绝对路径加载更直接但缺乏灵活性。推荐做法是在Java应用程序启动时通过-Djava.library.path参数指定DLL所在的目录。java -Djava.library.path./native_libs -cp . com.example.Main5.2 Java端调用示例创建一个测试类来使用我们的本地库。// Main.java package com.example; public class Main { static { // 加载本地库。库名是“NativeCalculator”对应“NativeCalculator.dll” // 注意不要加“.dll”后缀。 System.loadLibrary(NativeCalculator); // 或者使用绝对路径 // System.load(C:\\dev\\myproject\\x64\\Debug\\NativeCalculator.dll); } public static void main(String[] args) { NativeCalculator.initialize(); // 调用静态本地方法 NativeCalculator calc new NativeCalculator(); int sum calc.add(5, 3); System.out.println(5 3 sum); // 输出: 5 3 8 String msg calc.greet(World); System.out.println(msg); // 输出: Hello, World! int[] arr {1, 2, 3, 4, 5}; // 假设我们实现了incrementArray // calc.incrementArray(arr); // System.out.println(Arrays.toString(arr)); // 输出: [2, 3, 4, 5, 6] } }编译并运行这个Java程序如果一切配置正确你就能看到C代码的计算结果了。6. 高级主题内存管理、线程安全与性能优化当项目从“跑通”进入“用好”阶段以下几个高级话题就必须面对。6.1 本地引用与全局引用JNI中传递给本地方法的jobject,jstring,jarray等默认都是“本地引用”Local Reference。它们在本地方法返回后会自动被垃圾回收器识别并可能回收。但是如果你需要长时间持有这些对象比如在一个全局变量中缓存一个Java对象就必须创建“全局引用”Global Reference或“弱全局引用”Weak Global Reference。// 错误示例在全局变量中保存本地引用 jobject globalObj; // 错误的 JNIEXPORT void JNICALL Java_..._someMethod(JNIEnv* env, jobject obj) { globalObj obj; // obj是本地引用方法返回后可能失效 } // 正确示例创建全局引用 jobject globalObj; JNIEXPORT void JNICALL Java_..._someMethod(JNIEnv* env, jobject obj) { // 创建全局引用 globalObj env-NewGlobalRef(obj); // ... 后续可以使用globalObj ... } // 在适当的时候如JNI_OnUnload中必须删除全局引用 JNIEXPORT void JNICALL Java_..._cleanup(JNIEnv* env) { if (globalObj ! nullptr) { env-DeleteGlobalRef(globalObj); globalObj nullptr; } }重要原则对于通过JNI函数如NewObject,NewStringUTF,GetObjectArrayElement等创建的对象或者作为参数传入的、但你需要在方法返回后继续使用的对象如果需要跨本地方法调用或跨线程使用必须将其升级为全局引用。同时有NewGlobalRef就必须有对应的DeleteGlobalRef否则会导致Java对象无法被回收造成内存泄漏。6.2 多线程环境下的JNIJNIEnv指针 (env)是线程相关的。你不能在一个线程中存储另一个线程的JNIEnv*并在后者中使用它。如果需要在本地创建的新线程非由JVM创建的线程也称为“附着线程”中调用JNI函数你必须先将该线程“附着”Attach到JVM上获取一个属于该线程的JNIEnv*使用完毕后再“分离”Detach。// 假设在C创建的线程中 void* nativeThread(void* arg) { JavaVM* jvm ...; // 需要事先保存全局的JavaVM指针在JNI_OnLoad中获取 JNIEnv* env; // 将当前线程附着到JVM jint result jvm-AttachCurrentThread((void**)env, nullptr); if (result ! JNI_OK) { // 处理错误 return nullptr; } // 现在可以安全地使用env调用JNI函数了 // ... // 从JVM分离当前线程 jvm-DetachCurrentThread(); return nullptr; }如何获取全局的JavaVM*通常在库被加载时JVM会调用JNI_OnLoad函数JavaVM* g_jvm nullptr; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { g_jvm vm; // 保存全局JavaVM指针 // 可以在这里缓存类、方法ID等 return JNI_VERSION_1_8; // 返回你支持的JNI版本 }6.3 性能优化要点缓存ID字段ID、方法ID、类引用jclass的查找是比较耗时的操作。尽量在初始化阶段如JNI_OnLoad或一个静态初始化方法中查找一次并缓存为全局变量对于jclass需要是全局引用。减少跨界调用JNI调用本身有开销。避免在循环中频繁进行大量的、细粒度的JNI调用比如在C循环里多次读取Java数组的单个元素。应该一次性将数据如整个数组获取到本地内存处理完毕后再一次性写回。使用临界区Critical Section对于非常短暂且需要直接访问Java数组原始内存的操作可以考虑使用GetPrimitiveArrayCritical和ReleasePrimitiveArrayCritical。它们会尝试“钉住”数组避免复制但在此期间必须不能进行任何可能阻塞或导致垃圾回收的JNI调用。使用不当容易导致死锁需谨慎。直接字节缓冲区Direct ByteBuffer对于需要Java和C共享大量数据如图像、音频数据的场景使用java.nio.ByteBuffer.allocateDirect()创建的直接缓冲区是最高效的方式。C端可以通过GetDirectBufferAddress获取内存地址直接操作。7. 实战调试技巧与常见问题排查调试是JNI开发中最具挑战性的环节。问题往往表现为JVM崩溃Crash、访问违规Access Violation或神秘的UnsatisfiedLinkError。7.1 在Visual Studio 2019中调试DLL由于DLL是由Java进程java.exe加载的我们需要“附加”到Java进程进行调试。生成调试符号确保你的C项目在Debug配置下编译这会生成.pdb文件其中包含调试信息。启动Java程序以调试模式启动你的Java应用或者先启动它。附加到进程在VS2019中点击菜单栏的“调试” - “附加到进程...”。选择进程在进程列表中找到你的Java应用程序进程java.exe。你可能需要勾选“显示所有用户的进程”和“附加到”选择“本机代码”。点击“附加”。设置断点在你的C源文件中设置断点。触发调用在Java端执行会调用你本地方法的代码。如果一切顺利执行到断点处时VS2019会中断并显示当前的调用栈、变量值等信息你可以像调试普通C程序一样进行单步调试、查看内存。常见问题有时附加后断点显示为空心圆提示“当前不会命中断点。未加载此文档的符号”。这通常是因为DLL路径不对VS没有找到对应DLL的.pdb文件。确保Java加载的DLL就是你刚刚用VS编译出来的那个版本、路径。符号未加载在VS的“模块”窗口调试 - 窗口 - 模块中找到你的DLL右键选择“加载符号”然后手动指定.pdb文件路径。7.2 常见错误与解决方案下面是一个常见问题速查表帮助你快速定位问题。错误现象可能原因排查步骤与解决方案UnsatisfiedLinkError1. DLL未找到。2. DLL依赖项缺失。3. 方法签名不匹配。1. 检查java.library.path确认DLL所在目录在其中。使用System.out.println(System.getProperty(“java.library.path”));打印。2. 使用Dependency Walker或VS自带的dumpbin /dependents NativeCalculator.dll命令检查DLL的依赖项是否都存在。3. 使用javap -s -p com.example.NativeCalculator检查Java类中的方法签名与C函数名含包名和签名仔细比对。一个空格或分号错误都会导致此问题。JVM崩溃Crash1. 内存访问违规如空指针、数组越界。2. 堆栈损坏如缓冲区溢出。3. JNI调用使用错误的JNIEnv*多线程问题。1. 在VS中附加调试查看崩溃时的调用栈和寄存器。重点关注你的本地方法。2. 检查所有数组访问、指针操作是否越界。3. 确保在多线程环境下正确获取和使用JNIEnv*。error: a jni error has occurred通常出现在启动Java程序时是UnsatisfiedLinkError或其他链接错误在命令行下的通用提示。同上仔细检查UnsatisfiedLinkError的各个原因。查看完整的错误堆栈信息。内存泄漏1. 未成对调用Get/ReleaseStringUTFChars、Get/ReleaseArrayElements。2. 未删除全局引用 (DeleteGlobalRef)。3. 本地引用过多在循环中创建大量本地引用而未管理。1. 确保所有Get操作都有对应的Release。2. 确保全局引用在不用时被删除。3. 在本地方法中如果创建了大量本地引用比如在循环中NewObject可以适时调用env-DeleteLocalRef()手动删除或者使用env-PushLocalFrame()/env-PopLocalFrame()来管理本地引用的生命周期。性能低下1. 频繁的JNI跨界调用。2. 未缓存ID每次调用都查找。1. 采用批处理策略减少跨界调用次数。2. 在类初始化时缓存字段ID、方法ID和类引用。7.3 使用日志辅助调试在C代码中大量使用日志输出是定位问题的好方法。你可以使用printf输出到控制台如果Java进程有控制台但更推荐写入文件因为Java进程的控制台可能被重定向。#include fstream std::ofstream logfile(C:\\temp\\jnidebug.log, std::ios::app); logfile [ __LINE__ ] Entering Java_com_example_add std::endl;同时在Java端使用try-catch (Throwable t)包裹本地方法调用并打印异常堆栈可以捕获到一些由JNI调用抛出的Java异常。8. 项目构建与持续集成考量最后将JNI项目集成到更大的构建系统如Maven、Gradle或持续集成CI流水线中需要考虑自动化。8.1 使用CMake管理C项目对于复杂的项目使用CMake代替VS的解决方案文件.sln是更好的选择它更易于跨平台和集成到自动化脚本中。你可以在CMakeLists.txt中配置JDK路径、编译选项等。cmake_minimum_required(VERSION 3.10) project(NativeCalculator) # 查找JDK find_package(Java REQUIRED) find_path(JNI_INCLUDE_DIRS jni.h PATHS ${Java_INCLUDE_PATH} ${Java_INCLUDE_PATH2}) find_library(JVM_LIBRARY NAMES jvm PATHS ${Java_JVM_LIBRARY}) add_library(NativeCalculator SHARED src/NativeCalculatorImpl.cpp) target_include_directories(NativeCalculator PRIVATE ${JNI_INCLUDE_DIRS}) target_link_libraries(NativeCalculator ${JVM_LIBRARY})然后在CI服务器上调用CMake生成VS项目或直接编译。8.2 与Maven/Gradle集成你可以使用Maven的native-maven-plugin或Gradle的cpp插件来管理本地代码的编译。这些插件可以配置为在mvn package或gradle build时自动调用CMake/MSBuild进行编译并将生成的DLL打包到最终的JAR包中或者复制到资源目录。这实现了Java和本地代码构建的一体化。整个流程走下来从环境配置、代码编写、编译链接、调试排错到构建集成在Visual Studio 2019中完成C JNI开发虽然步骤繁多但每一步都有其明确的逻辑。最关键的还是对JNI内存管理规则获取与释放、引用类型的深刻理解以及对调试技巧的熟练掌握。一旦打通这个流程你就拥有了在Java生态中无缝集成C/C高性能代码的能力这对于解决计算密集型、硬件交互或复用遗留代码库的需求价值巨大。