在Java中,sun.misc.Unsafe可以认为是用于JDK内部使用的工具类,它将一些需要使用native语言实现的功能通过java方法暴露出来,这些方法比较“危险”,因为它们可以直接修改内存中的值。
通常情况下,我们并不能直接在程序中使用Unsafe,Unsafe的构造方法被私有化,语法层面上只能通过其提供的公共静态方法getUnsafe获取Unsafe实例:theUnsafe,theUnsafe在静态代码块被初始化,所以其是一个单例,如下所示:
public final class Unsafe {
//静态的、私有的实例字段
private static final Unsafe theUnsafe;
......
//私有化构造器
private Unsafe() {
}
......
//对外提供静态方法获取实例
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
//检查是否允许访问Unsafe
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
......
static {
......
//初始化Unsafe实例
theUnsafe = new Unsafe();
......
}
}
从Unsafe的代码中可以看出,通过getUnsafe方法获取Unsafe单例是唯一的“正规”途径,而该方法中对调用者classLoader做了检查,我们看一下检查代码:VM.isSystemDomainLoader:
public static boolean isSystemDomainLoader(ClassLoader var0) {
return var0 == null;
}
逻辑比较简单,主要是看classLoader是否为null,也就是判断该类是不是由BootStrap加载,如果不是的话,则不具有访问Unsafe的权限,随即抛出SecurityException异常。
import sun.misc.Unsafe;
public class Test{
public static void main(String[] args){
Unsafe unsafe = Unsafe.getUnsafe();
}
}
上述代码尝试获取一个Unsafe实例,会抛出异常异常:
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
虽然我们没有权限调用Unsafe#getUnsafe方法,但是我们还是有办法能够获取到Unsafe实例。既然theUnsafe字段在静态代码块中被初始化了,并且它是一个静态变量,那么我们可以直接通过反射获取该字段值。
在下面的代码中,我们先获取到了Unsafe实例,并且通过Unsafe#putInt方法将Test实例的value字段设置为100,然后通过Unsafe#getIntVolatile方法获取该值,并输出:
public class Test {
private int value;
public static void main(String[] args) throws Exception {
//获取theUnsafe字段
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
//私有变量设置访问权限
unsafeField.setAccessible(true);
//theUnsafe是静态变量,直接通过Field#get(null)获取
Unsafe theUnsafe = (Unsafe) unsafeField.get(null);
//创建Test实例
Test test = new Test();
//获取value字段在Test中的偏移量
long fieldOffset = theUnsafe.objectFieldOffset(Test.class.getDeclaredField("value"));
//直接操作该内存,设置值
theUnsafe.putInt(test, fieldOffset, 100);
//获取该偏移的值
System.out.println(theUnsafe.getIntVolatile(test, fieldOffset));
}
}
控制台输出为:100
Unsafe中的所有基础方法都属于native方法,这里我们就不一一解析了,就看一下JUC中经常使用的CAS源码实现即可,至于其它方法,读者朋友可以根据自身情况酌情研究。
在openjdk的源码中,Unsafe主要的实现代码在unsafe.cpp中,其中包含了native方法操作内存的实现细节。这里我们主要看看以下两个native方法的实现:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
我们首先看看compareAndSwapInt方法在openjdk中的实现:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
首先是JNIHandles::resolve(obj)方法。该方法定义在jniHandles.hpp中:
inline oop JNIHandles::resolve(jobject handle) {
oop result = (handle == NULL ? (oop)NULL : *(oop*)handle);
assert(result != NULL || (handle == NULL || !CheckJNICalls || is_weak_global_handle(handle)), "Invalid value read from jni handle");
assert(result != badJNIHandle, "Pointing to zapped jni handle area");
return result;
};
可以看到,该方法定义为一个内联函数,没有做什么复杂的事儿,就是把jobject参数转成了oop。
然后调用index_oop_from_field_offset_long(p, offset);方法,其中offset为修改的字段在对象所占内存中的偏移位置(相关信息可参考Java对象内存布局),最终得到的addr就是该字段在内存中的位置,这里可以简单理解为对象地址加上offset。
得到字段地址addr之后就会调用核心的Atomic::cmpxchg方法,该方法定义在atomic.hpp中,除了cmpxchg,还有xchg、cmpxchg_ptr等等,我们简单看一下:
class Atomic : AllStatic {
public:
// Atomic operations on jlong types are not available on all 32-bit
// platforms. If atomic ops on jlongs are defined here they must only
// be used from code that verifies they are available at runtime and
// can provide an alternative action if not - see supports_cx8() for
// a means to test availability.
// Atomically store to a location
inline static void store (jbyte store_value, jbyte* dest);
inline static void store (jshort store_value, jshort* dest);
inline static void store (jint store_value, jint* dest);
// See comment above about using jlong atomics on 32-bit platforms
inline static void store (jlong store_value, jlong* dest);
inline static void store_ptr(intptr_t store_value, intptr_t* dest);
inline static void store_ptr(void* store_value, void* dest);
inline static void store (jbyte store_value, volatile jbyte* dest);
inline static void store (jshort store_value, volatile jshort* dest);
inline static void store (jint store_value, volatile jint* dest);
// See comment above about using jlong atomics on 32-bit platforms
inline static void store (jlong store_value, volatile jlong* dest);
inline static void store_ptr(intptr_t store_value, volatile intptr_t* dest);
inline static void store_ptr(void* store_value, volatile void* dest);
// See comment above about using jlong atomics on 32-bit platforms
inline static jlong load(volatile jlong* src);
// Atomically add to a location, return updated value
inline static jint add (jint add_value, volatile jint* dest);
inline static intptr_t add_ptr(intptr_t add_value, volatile intptr_t* dest);
inline static void* add_ptr(intptr_t add_value, volatile void* dest);
// See comment above about using jlong atomics on 32-bit platforms
static jlong add (jlong add_value, volatile jlong* dest);
// Atomically increment location
inline static void inc (volatile jint* dest);
static void inc (volatile jshort* dest);
inline static void inc_ptr(volatile intptr_t* dest);
inline static void inc_ptr(volatile void* dest);
// Atomically decrement a location
inline static void dec (volatile jint* dest);
static void dec (volatile jshort* dest);
inline static void dec_ptr(volatile intptr_t* dest);
inline static void dec_ptr(volatile void* dest);
// Performs atomic exchange of *dest with exchange_value. Returns old prior value of *dest.
inline static jint xchg(jint exchange_value, volatile jint* dest);
static unsigned int xchg(unsigned int exchange_value, volatile unsigned int* dest);
inline static intptr_t xchg_ptr(intptr_t exchange_value, volatile intptr_t* dest);
inline static void* xchg_ptr(void* exchange_value, volatile void* dest);
// Performs atomic compare of *dest and compare_value, and exchanges *dest with exchange_value
// if the comparison succeeded. Returns prior value of *dest. Guarantees a two-way memory
// barrier across the cmpxchg. I.e., it's really a 'fence_cmpxchg_acquire'.
static jbyte cmpxchg (jbyte exchange_value, volatile jbyte* dest, jbyte compare_value);
inline static jint cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value);
// See comment above about using jlong atomics on 32-bit platforms
inline static jlong cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value);
static unsigned int cmpxchg(unsigned int exchange_value,
volatile unsigned int* dest,
unsigned int compare_value);
inline static intptr_t cmpxchg_ptr(intptr_t exchange_value, volatile intptr_t* dest, intptr_t compare_value);
inline static void* cmpxchg_ptr(void* exchange_value, volatile void* dest, void* compare_value);
};
cmpxchg实现在atomic.cpp中,最终会根据具体的宿主环境内联具体的实现,具体我们参照include的atomic.inline.hpp头文件:
// Linux
#ifdef TARGET_OS_ARCH_linux_x86
# include "atomic_linux_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_sparc
# include "atomic_linux_sparc.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_zero
# include "atomic_linux_zero.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_arm
# include "atomic_linux_arm.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_ppc
# include "atomic_linux_ppc.inline.hpp"
#endif
// Solaris
#ifdef TARGET_OS_ARCH_solaris_x86
# include "atomic_solaris_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_solaris_sparc
# include "atomic_solaris_sparc.inline.hpp"
#endif
// Windows
#ifdef TARGET_OS_ARCH_windows_x86
# include "atomic_windows_x86.inline.hpp"
#endif
// BSD
#ifdef TARGET_OS_ARCH_bsd_x86
# include "atomic_bsd_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_bsd_zero
# include "atomic_bsd_zero.inline.hpp"
#endif
#endif // SHARE_VM_RUNTIME_ATOMIC_INLINE_HPP
以x86的linux环境为例,其引入的是atomic_linux_x86.inline.hpp头文件,其中实现的内联函数较多,我们先只看看jint的cmpxchg函数实现:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
第一步调用了os::is_MP()函数,该函数定义在os.hpp中,主要是判断当前环境是否为多处理器环境:
// Interface for detecting multiprocessor system
static inline bool is_MP() {
assert(_processor_count > 0, "invalid processor count");
return _processor_count > 1 || AssumeMP;
}
使用__asm__ volatile嵌入汇编代码片段,一个内联汇编表达式分为四个部分,以:进行分隔。使用__asm__来声明表达式,volatile则保证指令不会被gcc优化影响,关于gcc内联汇编的更多内容不是本文的主题(博主也忘差不多了o(╯□╰)o),感兴趣的朋友可以自行了解。cmpxchgl指令则是x86的比较并交换指令,如果是多处理器会使用lock前缀,关于lock前缀,我们在总结volatile的博文中介绍过,其可以达到一个内存屏障的效果,也可以参照intel手册。
从上面也看到了,Atomic::cmpxchg的方法的返回值是内存中的
同样先在unsafe.cpp中找到compareAndSwapLong的实现:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapLong(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jlong e, jlong x))
UnsafeWrapper("Unsafe_CompareAndSwapLong");
Handle p (THREAD, JNIHandles::resolve(obj));
jlong* addr = (jlong*)(index_oop_from_field_offset_long(p(), offset));
if (VM_Version::supports_cx8())
return (jlong)(Atomic::cmpxchg(x, addr, e)) == e;
else {
jboolean success = false;
ObjectLocker ol(p, THREAD);
if (*addr == e) { *addr = x; success = true; }
return success;
}
UNSAFE_END
和compareAndSwapInt不一样的是,jlong类型的CAS操作需要判断宿主平台是否支持8字节的CAS操作,也就是通过VM_Version::supports_cx8方法进行判断。该方法定义在vm_version.hpp中:
// does HW support an 8-byte compare-exchange operation?
static bool supports_cx8() {
#ifdef SUPPORTS_NATIVE_CX8
return true;
#else
return _supports_cx8;
#endif
}
其中_supports_cx8默认为false,在相应的平台下会有对应的方法进行赋值,比如在x86下,会在vm_version_x86.cpp中调用supports_cmpxchg8()方法进行赋值,该方法定义在vm_version_x86.hpp中:
static bool supports_cmpxchg8() { return (_cpuFeatures & CPU_CX8) != 0; }
这个_cpuFeatures可以理解为cpu的一些属性,其在vm_version_x86.cpp中处理:如果支持CPUID,则会通过调用feature_flags()方法获取:
if (cpu_family() > 4) { // it supports CPUID
_cpuFeatures = feature_flags();
// Logical processors are only available on P4s and above,
// and only if hyperthreading is available.
_logical_processors_per_package = logical_processor_count();
}
我们来看一下feature_flags方法:
static uint32_t feature_flags() {
uint32_t result = 0;
if (_cpuid_info.std_cpuid1_edx.bits.cmpxchg8 != 0)
result |= CPU_CX8;
if (_cpuid_info.std_cpuid1_edx.bits.cmov != 0)
result |= CPU_CMOV;
if (_cpuid_info.std_cpuid1_edx.bits.fxsr != 0 || (is_amd() &&
_cpuid_info.ext_cpuid1_edx.bits.fxsr != 0))
result |= CPU_FXSR;
// HT flag is set for multi-core processors also.
if (threads_per_core() > 1)
result |= CPU_HT;
if (_cpuid_info.std_cpuid1_edx.bits.mmx != 0 || (is_amd() &&
_cpuid_info.ext_cpuid1_edx.bits.mmx != 0))
result |= CPU_MMX;
if (_cpuid_info.std_cpuid1_edx.bits.sse != 0)
result |= CPU_SSE;
if (_cpuid_info.std_cpuid1_edx.bits.sse2 != 0)
result |= CPU_SSE2;
if (_cpuid_info.std_cpuid1_ecx.bits.sse3 != 0)
result |= CPU_SSE3;
if (_cpuid_info.std_cpuid1_ecx.bits.ssse3 != 0)
result |= CPU_SSSE3;
if (_cpuid_info.std_cpuid1_ecx.bits.sse4_1 != 0)
result |= CPU_SSE4_1;
if (_cpuid_info.std_cpuid1_ecx.bits.sse4_2 != 0)
result |= CPU_SSE4_2;
if (_cpuid_info.std_cpuid1_ecx.bits.popcnt != 0)
result |= CPU_POPCNT;
if (_cpuid_info.std_cpuid1_ecx.bits.avx != 0 &&
_cpuid_info.std_cpuid1_ecx.bits.osxsave != 0 &&
_cpuid_info.xem_xcr0_eax.bits.sse != 0 &&
_cpuid_info.xem_xcr0_eax.bits.ymm != 0) {
result |= CPU_AVX;
if (_cpuid_info.sef_cpuid7_ebx.bits.avx2 != 0)
result |= CPU_AVX2;
}
if (_cpuid_info.std_cpuid1_edx.bits.tsc != 0)
result |= CPU_TSC;
if (_cpuid_info.ext_cpuid7_edx.bits.tsc_invariance != 0)
result |= CPU_TSCINV;
if (_cpuid_info.std_cpuid1_ecx.bits.aes != 0)
result |= CPU_AES;
if (_cpuid_info.sef_cpuid7_ebx.bits.erms != 0)
result |= CPU_ERMS;
if (_cpuid_info.std_cpuid1_ecx.bits.clmul != 0)
result |= CPU_CLMUL;
// AMD features.
if (is_amd()) {
if ((_cpuid_info.ext_cpuid1_edx.bits.tdnow != 0) ||
(_cpuid_info.ext_cpuid1_ecx.bits.prefetchw != 0))
result |= CPU_3DNOW_PREFETCH;
if (_cpuid_info.ext_cpuid1_ecx.bits.lzcnt != 0)
result |= CPU_LZCNT;
if (_cpuid_info.ext_cpuid1_ecx.bits.sse4a != 0)
result |= CPU_SSE4A;
}
return result;
}
从方法体中可以看到,如果从CPUID中判断到支持cmpxchg8,那么会将result和CPU_CX8进行按位或,CPU_CX8定义在枚举中,表示哪一位是cmpxchg8是否支持的标识:
enum {
CPU_CX8 = (1 << 0), // next bits are from cpuid 1 (EDX)
CPU_CMOV = (1 << 1),
CPU_FXSR = (1 << 2),
CPU_HT = (1 << 3),
CPU_MMX = (1 << 4),
CPU_3DNOW_PREFETCH = (1 << 5), // Processor supports 3dnow prefetch and prefetchw instructions
// may not necessarily support other 3dnow instructions
CPU_SSE = (1 << 6),
CPU_SSE2 = (1 << 7),
CPU_SSE3 = (1 << 8), // SSE3 comes from cpuid 1 (ECX)
CPU_SSSE3 = (1 << 9),
CPU_SSE4A = (1 << 10),
CPU_SSE4_1 = (1 << 11),
CPU_SSE4_2 = (1 << 12),
CPU_POPCNT = (1 << 13),
CPU_LZCNT = (1 << 14),
CPU_TSC = (1 << 15),
CPU_TSCINV = (1 << 16),
CPU_AVX = (1 << 17),
CPU_AVX2 = (1 << 18),
CPU_AES = (1 << 19),
CPU_ERMS = (1 << 20), // enhanced 'rep movsb/stosb' instructions
CPU_CLMUL = (1 << 21) // carryless multiply for CRC
} cpuFeatureFlags;
这里可以看到,用_cpuFeatures的最低位标识cmpxchg8,如果支持,那么将该位通过 | 操作设置为1,判断的时候像这样简单判断即可:
_cpuFeatures & CPU_CX8 != 0
我们回到Unsafe_CompareAndSwapLong处,如果支持cmpxchg8的话,那么和jint类似,会调用Atomic::cmpxchg方法实现CAS操作,一下是linux_x86下的内联实现:
inline jlong Atomic::cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value) {
bool mp = os::is_MP();
__asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
注意前面jint的CAS使用的是cmpxchgl指令,而此处使用的是cmpxchgq指令。当然,如果不支持cmpxchg8的话,就只能自己处理,而且需要保证原子性,所以使用了ObjectLocker,上锁之后实现比较并交换逻辑即可:
jboolean success = false;
ObjectLocker ol(p, THREAD);
if (*addr == e) { *addr = x; success = true; }
return success;
最后,CAS操作返回的是布尔类型的结果,使用ObjectLocker时,从代码上就很好理解,先比较地址上的值,如果相等则交换,返回true,否则返回false。而使用Atomic::cmpxchg时,结果是cmpxchg返回值和期望值比较的结果:
(Atomic::cmpxchg(x, addr, e)) == e
回到上面cmpxchg的内联实现,其返回的是exchange_value。关于cmpxchgl和cmpxchgq指令,如果期望值和当前值相等的话,那么会将新值写到内存地址,覆盖当前值,并且返回被覆盖的值(旧值,其实也是期望值);如果期望值和当前值不相等,那么会返回最新的当前值。所以这个操作叫做比较并交换,不论比较的结果如何,都会“换出”内存地址的当前值。
所以,只有当返回的exchange_value和我们的期望值相等时,才表示指令执行成功了,否则表示该内存上的值被其它线程更新了,指令失败。
现在我们再回过头来看JNI(Java Native Interface),即Java本地接口,它能将Java与c/c++等本地代码进行集成,使得Java代码能够与其它编程语言进行相互操作。JNI是一个接口,类似于虚拟机规范,其并没有限制实现细节。
某一些功能我们可能无法用Java语言直接实现(或者说高效实现),那么就需要借助其他本地语言实现,我们只需要在Java中定义好接口(native),JVM在调用方法时会根据指定的方法名从本地库中寻找具体的实现,这个实现可能由c++编写。
我们这里实现一个JNI调用的流程。
首先,我们编写Java文件,TestJNI.java:
class TestJNI{
public native void say();
static{
System.loadLibrary("TestLib");
}
public static void main(String[] args){
new TestJNI().say();
}
}
在TestJNI.java中,我们定义了一个native方法say(),该方法我们会以c++实现。在static静态代码块中加载动态库,该库中需要包含say方法的本地实现,所以接下来我们需要组建TestLib动态库(TestLib.dll)。dll即Dynamic Link Library,可以想象为我们Java中的jar包,里面包含一系列可供他人调用的公用函数。
注:这里为了简单,我直接把TestJNI.java放到桌面上操作。
我们的native方法是定义在TestJNI.java中的,现在要用c++实现该方法,需要定义该方法的头文件,并且JNI对native方法的方法名有规范,并不能随便写,因为JVM寻找本地实现的时候有自己的规则。这样看来编写头文件还比较麻烦,好在jdk为我们提供了指令:javah,以快速创建该头文件。
C:\Users\Administrator\Desktop>javah TestJNI
C:\Users\Administrator\Desktop>
调用javah TestJNI之后,会生成一个TestJNI.h文件,我们来看一下文件内容:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class TestJNI */
#ifndef _Included_TestJNI
#define _Included_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: TestJNI
* Method: say
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_TestJNI_say
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
我们看到TestJNI.h中引入了jni.h,该文件位于%JAVA_HOME%\include目录中:
文件中定义了一个Java_TestJNI_say方法,JNIEnv*表示的是JNI本身,通过它可以调用jni.h中定义好的函数,jobject表示当前对象(实例对象或者class)。我们不用太过关注,知道这里定义了一个接口就行了。
有了头文件,接下来我们要开始编写实现、生成dll了,由于博主环境为64位,所以需要生成64位环境的dll,使用的IDE是Visual Studio 2010。
首先,选择文件-新建-项目,选择模板Visual C++、Win32控制台应用程序,项目名称为TestJNI.java中加载的库名TestLib,同时我们要清楚项目位置,这里使用的是默认配置:
然后点击确定:
这里点击“下一步”,然后再应用程序设置中,选择应用程序类型为DLL,附加选项为空项目,点击完成:
这样项目就创建好,接下来我们设置64位环境。首先进入配置管理器:
平台在win32处下拉,选择新建:
在新平台处选择64,从Win32处复制,勾选创建新的项目平台:
然后点击确定:
然后关闭配置管理器。
接下来我们创建头文件,在项目上右键-添加-类,选择C++类:
选择之后,点击添加。然后在向导中输入类名,这里我们使用前面定义的TestJNI就行了,点击完成。
这里就创建了TestJNI.h和TestJNI.cpp文件:
然后我们直接将前面使用javah命令生成的TestJNI.h文件内容拷贝到当前项目的TestJNI.h中,由于需要引用jni.h,所以我们需要将jni.h从%JAVA_HOME%\include目录中拷贝一份。为了简单,这里直接将jni.h放到TestJNI项目中,和TestJNI.h处于同一目录,所以我们需要修改TestJNI.h中引用jni.h的方法,改为相对路径引入,即<>改为"":
这里我们打开jni.h简单看一下,发现其中需要引入jni_md.h:
所以我们需要将jni.h文件和jni_md.h都拷贝到项目中,和TestJNI.h同目录(jni_md.h在%JAVA_HOME%\include\win32目录中):
接下来编写TestJNI.cpp文件,在该文件中实现Java_TestJNI_say方法:简单输出hello world
#include "TestJNI.h"
JNIEXPORT void JNICALL Java_TestJNI_say (JNIEnv *env , jobject obj){
printf("hello world");
}
到这里我们代码就编写完毕了,接下来进入顶部导航栏:生成-重新生成解决方案
下方输出区域会显示执行结果:
这里可能会被360等软件拦截,允许即可。生成成功之后,我们进入项目目录的x64\Debug目录中,可以看到已经生成了dll文件了:
到这里我们TestLib.dll动态链接库已经创建好了,接下来将其复制到TestJNI.java目录中,当然我们这里是在桌面上:
然后我们打开命令窗口,先javac TestJNI.java编译,然后java TestJNI运行代码:
我们看到,输出了hello world,native方法调用成功。