Java 中的 native 方法
Java 中的 native 方法
最近在学习 Thread
类源码时,发现类中第一行的 registerNatives() 方法在很多 Java
类中都出现过,比如 Object
类、System
类、Class
类等中都有,而这个方法又是使用 native 关键字修饰,代码中没有具体的实现,故对此比较感兴趣,想知道其究竟有什么作用,为何这么多类中都有这个方法,本文则对学习过程做下记录。 下面是类中具体的方法:
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
既然 registerNatives() 方法是 native 修饰的本地方法,那我们就从本地方法开始,探究内部的实现。在***《深入Java虚拟机》***一书中,作者Bill Venners
在1.3.1节对本地方法有这样一段描述:
Java 中有两种方法:Java 方法和本地方法。Java 方法是由 Java 语言编写,编译成字节码,存储在 class 文件中的。本地方法是由其他语言(比如 C,C++,或者汇编语言)编写的,编译成和处理器相关的机器代码。本地方法保存在动态链接库中,格式是各个平台专用的。Java 方法是与平台无关的,但是本地方法却不是。运行中的 Java 程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。如下图中,本地方法是联系 Java 程序和底层操作系统的连接方法。通过本地方法,Java 程序可以直接访问底层操作系统的资源。
由上述可知,本地方法的实现是由其他语言编写并保存在动态库中,因而在 Java 类中不需要方法的实现。registerNative() 方法就是一个本地方法,但其又有别于一般的本地方法,根据方法名称我们可以猜到这是用来注册本地方法的,当 Thread 类被加载时,通过 static{} 静态代码块调用本地方法进行注册。这里可能会产生一些疑问:registerNative() 究竟注册了哪些方法?为什么要注册?具体又是如何实现注册的?
1. registerNative() 究竟注册了哪些方法?
带着这个问题,我们打开 Thread 类的源码,可以看到除了 registerNative() 方法外,Thread 类中还存在着 currentThread()、yield()、sleep(long millis)、start0()、interrupted() 等本地方法,我们再打开其他类也可以看到,Object 类中含有 getClass()、hashCode()、clone() 等本地方法,System 类中含有 currentTimeMillis()、nanoTime() 等本地方法。这里可以先猜测一下,registerNative() 方法注册的就是除了其本身外的这些本地方法。
现在打开 OpenJDK11 的源码包来验证一下:
打开 src 目录,进入到 java.base.share 下可以看到有一个 native 的包,这里应该就是 Java 本地方法的实现。
打开其中的 libjava 包,可以看到很多熟悉的名称,诸如上面提到的 Object.c 、Thread.c 、System.c 等,这些都是使用 C 语言实现的。
下面是 Thread.c 的代码:
#include "jni.h"
#include "jvm.h"
#include "java_lang_Thread.h"
#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"
#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},
{"suspend0", "()V", (void *)&JVM_SuspendThread},
{"resume0", "()V", (void *)&JVM_ResumeThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield", "()V", (void *)&JVM_Yield},
{"sleep", "(J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
{"countStackFrames", "()I", (void *)&JVM_CountStackFrames},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},
{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
{"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};
#undef THD
#undef OBJ
#undef STE
#undef STR
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
可以看到 Thread 类中的本地方法都在这里面, registerNative() 方法注册的正是这些本地方法,我们上面的猜测得到了验证,registerNative() 注册的方法就是该类所包含的除了 registerNative() 方法外的所有本地方法。
2. 为什么要使用registerNative() 进行注册?
针对这个问题,我们在**《 The Java Native Interface -- Programmer’s Guide and Specification 》**一书的8.3节 Registering Native Methods
中找到如下描述:
Before an application executes a native method it goes through a two-step process to load the native library containing the native method implementation and then link to the native method implementation:
- System.loadLibrary locates and loads the named native library. For example, System.loadLibrary("foo") may cause foo.dll to be loaded on Win32.
- The virtual machine locates the native method implementation in one of the loaded native libraries. For example, a Foo.g native method call requires locat-ing and linking the native function Java_Foo_g, which may reside in foo.dll .
This section will introduce another way to accomplish the second step. Instead of relying on the virtual machine to search for the native method in the already loaded native libraries, the JNI programmer can manually link native methods by registering a function pointer with a class reference, method name, and method descriptor:
JNINativeMethod nm; nm.name = "g"; /* method descriptor assigned to signature field */ nm.signature = "()V"; nm.fnPtr = g_impl; (*env)->RegisterNatives(env, cls, &nm, 1);
The above code registers the native function g_impl as the implementation of the Foo.g native method:
void JNICALL g_impl(JNIEnv *env, jobject self);
The native function g_impl does not need to follow the JNI naming convention because only function pointers are involved, nor does it need to be exported from the library (thus there is no need to declare the function using JNIEXPORT ). The native function g_impl must still, however, follow the JNICALL calling convention.
The RegisterNatives function is useful for a number of purposes:
• It is sometimes more convenient and more efficient to register a large number of native method implementations eagerly, as opposed to letting the virtual machine link these entries lazily.
• You may call RegisterNatives multiple times on a method, allowing the native method implementation to be updated at runtime.
• RegisterNatives is particularly useful when a native application embeds a virtual machine implementation and needs to link with a native method implementation defined in the native application. The virtual machine would not be able to find this native method implementation automatically because it only searches in native libraries, not the application itself.
根据上述内容可以得知,一个 Java 应用程序想要调用一个本地方法,需要执行两个步骤:
1、首先使用 System.loadLibrary() 将包含本地方法实现的动态库文件加载到内存中。
2、当 Java 程序需要调用到本地方法时,虚拟机在加载的动态库文件中定位并链接这个本地方法,从而得以执行该本地方法。
registerNative() 方法的作用就是取代第二步,在程序加载启动时就将本地方法主动链接到调用方,当 Java 程序需要调用本地方法时就可以直接调用,而不需要虚拟机再去定位并链接本地方法。
同时上述文中还总结了一些使用 registerNatives() 方法的优点:
-
通过 registerNatives() 方法在类被加载的时候就主动将本地方法链接到调用方,这比当本地方法被调用时再由虚拟机去定位和链接更方便有效。
-
如果本地方法在程序运行时被更新了,可以通过多次调用 registerNative() 方法来加载更新。
-
函数命名自由,使用 registerNative() 方法,在定义本地方法实现的时候,不用去遵循 JNI 命名规范。
3. registerNative()是如何实现注册的?
对于这个问题,我们重新回到 Thread.c 及 Object.c 文件中看下 registerNative() 方法:
/*
* Stuff for dealing with threads.
* originally in threadruntime.c, Sun Sep 22 12:09:39 1991
*/
#include "jni.h"
#include "jvm.h"
#include "java_lang_Thread.h"
#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"
#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},
{"sleep", "(J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
/** 此处省略部分方法... */
};
#undef THD
#undef OBJ
#undef STE
#undef STR
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
/*-
* Implementation of class Object
*
* former threadruntime.c, Sun Sep 22 12:09:39 1991
*/
#include <stdio.h>
#include <signal.h>
#include <limits.h>
#include "jni.h"
#include "jni_util.h"
#include "jvm.h"
#include "java_lang_Object.h"
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}
由上面的源码可以看到 registerNatives() 调用的都是 (*env)->RegisterNatives(env, cls, methods, sizeof(methods)/sizeof(methods[0]));
,上述代码开头 #include "jni.h"
引入 jni.h
头文件,我们进入该头文件下可以看到 JNINativeMethod
、JNIEnv
JNIInvokeInterface_
等结构体的定义。
/*
* used in RegisterNatives to describe native method name, signature,
* and function pointer.
*/
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
/*
* JNI Native Method Interface.
*/
struct JNINativeInterface_;
struct JNIEnv_;
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
我们抽出其中 JNINativeInterface_
和 JNIEnv_
结构体中的关键方法,可以看到 RegisterNative()
调用的实际是由 JNINativeInterface_
实现的函数。
struct JNINativeInterface_ {
jint (JNICALL *RegisterNatives)
(JNIEnv *env, jclass clazz, const JNINativeMethod *methods,
jint nMethods);
};
struct JNIEnv_ {
const struct JNINativeInterface_ *functions;
#ifdef __cplusplus
jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,
jint nMethods) {
return functions->RegisterNatives(this,clazz,methods,nMethods);
}
#endif /* __cplusplus */
};
打开 jni.h
头文件对应的 jni.cpp
文件中的实现函数代码:
struct JNINativeInterface_ jni_NativeInterface = {
jni_RegisterNatives,
};
// 注册Java系统类中的本地方法
JNI_ENTRY(jint, jni_RegisterNatives(JNIEnv *env, jclass clazz,
const JNINativeMethod *methods,
jint nMethods))
JNIWrapper("RegisterNatives");
HOTSPOT_JNI_REGISTERNATIVES_ENTRY(env, clazz, (void *) methods, nMethods);
jint ret = 0;
DT_RETURN_MARK(RegisterNatives, jint, (const jint&)ret);
// 加载对应的类并转换成Klass对象
Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz));
// 循环对数组中的本地方法进行处理
for (int index = 0; index < nMethods; index++) {
const char* meth_name = methods[index].name;
const char* meth_sig = methods[index].signature;
int meth_name_len = (int)strlen(meth_name);
// The class should have been loaded (we have an instance of the class
// passed in) so the method and signature should already be in the symbol
// table. If they're not there, the method doesn't exist.
// 方法名
TempNewSymbol name = SymbolTable::probe(meth_name, meth_name_len);
// 方法签名
TempNewSymbol signature = SymbolTable::probe(meth_sig, (int)strlen(meth_sig));
// 如果没找到该方法则抛出java.lang.NoSuchMethodError()
if (name == NULL || signature == NULL) {
ResourceMark rm;
stringStream st;
st.print("Method %s.%s%s not found", k->external_name(), meth_name, meth_sig);
// Must return negative value on failure
THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), -1);
}
// 执行注册本地方法
bool res = register_native(k, name, signature,
(address) methods[index].fnPtr, THREAD);
if (!res) {
ret = -1;
break;
}
}
return ret;
JNI_END
static bool register_native(Klass* k, Symbol* name, Symbol* signature, address entry, TRAPS) {
// 找到对应的方法
Method* method = k->lookup_method(name, signature);
if (method == NULL) {
ResourceMark rm;
stringStream st;
st.print("Method '");
Method::print_external_name(&st, k, name, signature);
st.print("' name or signature does not match");
THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
}
// 判断输入的方法是否是一个本地方法
if (!method->is_native()) {
// trying to register to a non-native method, see if a JVM TI agent has added prefix(es)
// 检查JVMTI是否指定native方法前缀,如果不存在则抛出 NoSuchMethodError 异常
method = find_prefixed_native(k, name, signature, THREAD);
if (method == NULL) {
ResourceMark rm;
stringStream st;
st.print("Method '");
Method::print_external_name(&st, k, name, signature);
st.print("' is not declared as native");
THROW_MSG_(vmSymbols::java_lang_NoSuchMethodError(), st.as_string(), false);
}
}
if (entry != NULL) {
// 设置本地方法
method->set_native_function(entry,
Method::native_bind_event_is_interesting);
} else {
method->clear_native_function();
}
if (PrintJNIResolving) {
ResourceMark rm(THREAD);
tty->print_cr("[Registering JNI native method %s.%s]",
method->method_holder()->external_name(),
method->name()->as_C_string());
}
return true;
}
4. 如何实现自定义的本地方法?
① 首先在 Java 类中声明一个被 native
关键字修饰的方法,然后在 static
静态代码块中使用 System.loadLibrary()
方法导入一个外部动态链接库。
package com.goldcard.custom;
/**
* desc: for custom goldcard native method
*
* @author dongYu
* @date 2021/08/20
*/
public class Goldcard {
// 使用静态代码块导入动态链接库
static {
// 注意:loadLibrary的参数必须和动态链接库名一致(不包括后缀名)
System.loadLibrary("GoldcardLibrary");
}
// 使用 native 声明一个本地方法
public native void sayHello();
// 此处为了方便后续编译执行代码
public static void main(String[] args) {
new Goldcard().sayHello();
}
}
② 在命令行中使用 javac -h . Goldcard.java
命令编译 Java 源文件,注意中间的 .
不能省略,它代表了在当前路径下生成编译产生的头文件,当然其实你可以把它换成你想要的路径,但为了方便,这里推荐直接使用点,生成字节码文件和头文件,编译完成后的目录结构如下图:
说明:如果使用的 jdk 版本过低,可能没有 javac -h
命令,可以使用 javac className.java
先编译 Java 代码,然后使用 javah -jni className
命令生成头文件。
③ 打开刚才编译生成的 com_goldcard_custom_Goldcard.h
头文件,注意,将 #include <jni.h>
修改为 #include "jni.h"
,方便后续后续导入 jni.h
文件。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class com_goldcard_custom_Goldcard */
#ifndef _Included_com_goldcard_custom_Goldcard
#define _Included_com_goldcard_custom_Goldcard
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_goldcard_custom_Goldcard
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
其中上面代码中的 JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello(JNIEnv *, jobject)
函数就是我们要实现的 sayHello
方法。
因为我们生成的头文件中包含了一个jni.h
的头文件,后续编译的话需要导入这个文件,所以我们从 jdk 的安装包中找到这个头文件,并将其复制到我们代码所在目录下(注:如果不想复制的话可使用 gcc -l 命令链接到指定的库文件,具体方法可自行百度):
打开 jni.h
文件可以看到,其中又包含了一个 #include "jni_md.h"
的文件,这个文件这可以在上面截图的 win32
的目录下找到:
完成后我们使用 c++
语言去实现上面提到的 JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello(JNIEnv *, jobject)
的这个函数:
#include "com_goldcard_custom_Goldcard.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_com_goldcard_custom_Goldcard_sayHello
(JNIEnv *, jobject)
{
printf(">>> Hello Goldcard <<<");
return;
}
因为这个函数是声明在 com_goldcard_custom_Goldcard.h
这个头文件中,而且打印输入用到了 printf()
函数,所以在文件头部我们导入这两个文件。
最后在命令行中使用 g++ --share goldcardLibrary.cpp -o goldcardLibrary.dll
命令生成动态链接库文件,注意:生成的动态库名称需要与 Java 代码里的 System.loadLibrary("文件名")
中的文件名保持一致。
生成的文件目录结构如下图所示:
④ 测试验证
在 IDEA
中新建一个测试类 DemoTest.java
文件,调用 Goldcard.java
中的本地方法
直接运行后,系统报 如下异常:
此异常有两种解决方式:
- 1、将生成的动态库文件放入
java.library.path
的目录中。 - 2、在 IDEA 的运行启动参数的 VM 参数配置
-Djava.library.path=动态库所在目录
即可。
这里采用第二种方式执行:
运行程序,最终输出 >>> Hello Goldcard <<<
的执行结果:
5. 附录
1、C++环境配置
2、调用jni的两种方法javah和RegisterNatives
5、The Java Native Interface — Programmer’s Guide and Specification PDF
10、后端【JVM源码探索】深入registerNative()底层实现
#native(1)#Java(6)评论