转载

Java之Thread源码registerNatives()深入理解

前言:阅读JDK源码可以看到,registerNatives()方法存在于Object类、Class类、ClassLoader类等常用的类中。其方法的定义如下:

private static native void registerNatives();
    static {
        registerNatives();
    }
复制代码

可以得到registerNatives()是native方法,其实现是由C/C++实现的,要深入理解registerNatives方法的含义与作用,首先需要对Java中的native关键字有深入认识(有相关基础的同学可以直接略过这一小节)。

1. Native Method

​ 对于native Method关键字官方文档的解释如下:

A native method is a Java method whose implementation is provided by non-java code.
复制代码

Native method是由非Java语言实现的方法。通常Java中的native方法的常是C/C++实现,Java中提供了与其他语言通信的API即JNI(Java Native Interface)。如果要使用Java调用其它语言的函数,就必须遵循JNI的API约定。

1.1 Native 使用规则

  1. native标识符除不能与abstract联用外,可以与其它标识符联用;
  2. native method方法可以返回任何java类型,包括非基本类型,也可以进行异常控制;
  3. 如果含有native method方法的类被继承,子类会继承这个native method方法,也可以使用java语言重写这个方法;
  4. 如果一个native method方法被fianl标识,它被继承后不能被重写。

1.2 Native Method实现步骤

native()

这里只是简单的介绍native相关知识点,如果需要更多详细的资料可以自行查阅相关资料。

阅读以上资料可知, registerNatives() 方法是由其它语言实现的。通过该方法名可以猜测到 registerNatives() 的作用是注册本地方法。在静态代码块中调用registerNatives(),可以得出当类加载时就进行注册。看到这里相信各位同学心中肯定会出现疑惑,为什么需要注册Native方法? registerNatives() 是如何注册本地方法?

这里查阅《The Java Native Interface Programmer’s Guide and Specification》,得到以下片段的说明。

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:

1.System.loadLibrary locates and loads the named native library. For example, System.loadLibrary("foo") may cause foo.dll to be loaded on Win32.

2.The virtual machine locates the native method implementation in one of the loaded native libraries. For example, a Foo.g native method call requires locating and linking the native function Java_Foo_g, which may reside in foo.dll.

复制代码

通过上面的解释资料可知,在调用本地方法之前,Java会经历两个步骤加载本地方法的实现库,第一步是使用System.loadLibrary()将包含本地方法实现的动态文件加载进内存;第二步是当Java程序需要调用本地方法时,虚拟机在加载的动态文件中定位并链接该本地方法,从而得以执行本地方法。下面我将通过实现一个native method的demo详细解释这段话的含义。

2. 使用JNI实现Native方法

public class ByteCodeEncryptor {
	public native static byte[] encrypt(byte[] text);
	static {
		System.loadLibrary("byteCodeEncryptor.dll");
	}
	public static void main(String args[]) {
		String test = "hello world";
		System.out.println(encrypt(test.getBytes()));
	}
}
分析:以上代码的作用是使用本地方法encrypt()实现加密;
复制代码

使用Javac命令生成以上Java源文件的头文件(.h)

#include <jni.h>
#ifndef _Included_com_seaboat_bytecode_ByteCodeEncryptor
#define _Included_com_seaboat_bytecode_ByteCodeEncryptor
#ifdef __cplusplus
extern "C" {
#endif
// 在 Windows 中编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加__declspec(dllexport)标识,表示将该函数导出在外部可以调用
JNIEXPORT jbyteArray JNICALL Java_com_individual_thread_register_ByteCodeEncryptor_encrypt
  (JNIEnv *, jclass, jbyteArray);
#ifdef __cplusplus
}
#endif
#endif
复制代码

实现头文件中的encrypt方法

#include "com_individual_thread_register_ByteCodeEncryptor.h"
#include "jni.h"

void encode(char *str)
{
    unsigned int m = strlen(str);
    for (int i = 0; i < m; i++)
    {
        str[i] = str[i]+4;
    }

}

extern"C" JNIEXPORT jbyteArray JNICALL
Java_com_individual_thread_register_ByteCodeEncryptor_encrypt(JNIEnv *env, jclass cla,jbyteArray text)
{
    char* dst = (char*)env->GetByteArrayElements(text, 0);
    encode(dst);
    env->SetByteArrayRegion(text, 0, strlen(dst), (jbyte *)dst);
    return text;
}
说明:
第一个参数:JNIEnv* 是定义任意native函数的第一个参数(包括调用JNI的RegisterNatives函数注册的函数),指向JVM 函数表的指针,函数表中的每一个入口指向一个JNI 函数,每个函数用于访问JVM中特定的数据结构。
第二个参数:调用Java中native方法的实例或Class对象,如果这个native方法是实例方法,则该参数是 jobject,如果是静态方法,则是 jclass。
第三个参数:Java 对应 JNI 中的数据类型,Java 中 String 类型对应 JNI 的 jstring 类型,(Java于JNI数据类型的映射关系)。
函数返回值类型:夹在 JNIEXPORT 和 JNICALL 宏中间的 jbyteArray,表示函数的返回值类型,对应Java的byte[]类型。
在Java中调用encrypt()本地方法即可得到加密后的结果。
复制代码

注意:实现native方法需要本地的C/C++源文件的方法名的格式必须为:Java_完整类名_方法名,包名的 . 号,以 _ 表示_method,这是因为JVM中对本地方法名有相应的规定,在使用JNI时需要遵守。 比如上面编写一个Java类提供本地加密的方法,其中加密方法为本地方法,实现是在byteCodeEncryptor动态库,那么它本地对应的函数名为Java_com_individual_thread_register_ByteCodeEncryptor_encrypt。

3. 使用JNI_onload函数实现Native方法

使用JNI_onload实现Native方法,首先需要知道JNI_OnLoad函数。其在JVM执行System.loadLibrary方法时JNI_OnLoad函数被调用,可以在该方法中调用registerNatives()函数注册本地函数。首先JNI_OnLoad的使用步骤:

  1. 在c/c++文件中定义并实现对应java中声明的本地方法,方法名称可随意,但参数类型和参数个数必须一样;
  2. 创建声明JNINativeMethod类型的数组,其值为需要动态加载映射的本地方法。

以下为JNI_onload方法实现本地加密

3.1 Java中声明的Native方法

public class RegisterTest {
	static {
		System.loadLibrary("registerTest.dll");
	}
	public native static byte[] encrypt(byte[] text);
	public static void main(String args[]) {
		String test = "hello  world";
		System.out.println(encrypt(test.getBytes()));
	}
}
复制代码

3.2 使用JNI_onload实现encrypt方法

#include <jni.h>
#include <string>

JNIEXPORT jbyteArray JNICALL encrypt
(JNIEnv *env, jclass cla, jbyteArray text) {
	char* dst = (char*)env->GetByteArrayElements(text, 0);
	unsigned int m = strlen(dst);
	for (int i = 0; i < m; i++)
	{
		dst[i] = dst[i] + 4;
	}
	env->SetByteArrayRegion(text, 0, strlen(dst), (jbyte *)dst);
	return text;
}
/**
第一个参数:encrypt 是java中的方法名称
第二个参数:([B)[B  是java中方法的签名,可以通过javap -s -p 类名.class 查看
第三个参数: (jstring *)encrypt  (返回值类型)映射到native的方法名称
*/
static JNINativeMethod methods[] = {
	{ "encrypt", "([B)[B", (jbyteArray *) encrypt}
};

static jclass myClass;
// 这里是java调用C的存在Native方法的类路径

static const char* const className = "com/individual/thread/register/RegisterTest";

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
	
	JNIEnv* env = NULL; //注册时在JNIEnv中实现的,所以必须首先获取它
	jint result = -1;
	if (vm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK) { //从JavaVM获取JNIEnv,一般使用1.4的版本
		return -1;
	}
	// 获取映射的java类
	myClass = env->FindClass(className);
	if (myClass == NULL)
	{
		printf("cannot get class:%s/n", className);
		return -1;
	}
	// 通过RegisterNatives方法动态注册登记

	if (env->RegisterNatives(myClass, methods, sizeof(methods) / sizeof(methods[0])) < 0)
	{
		printf("register native method failed!/n");
		return -1;
	}
	return JNI_VERSION_1_4; //必须返回版本,否则加载会失败。
}
复制代码

将.cpp文件打包成dll文件就可以在Java中调用成功,使用JNI_onlad实现native方法的结果与普通JNI实现native的结果相同. 仔细阅读以上两种实现方式可以得出其区别,使用JNI_onlad可以在C/C++实现native方法时其方法名可以不与生成的头文件方法名相同,相比而言第二种方式更加便捷。除此之外,它们之间的效率也不一样。查阅相关资料使用JNI_onload的方式实现native方法更加高效,其原因如下:

  1. 使用JNI传统方式,当Java类调用本地函数时,通常是依靠虚拟机去动态寻找链接库中的本地函数(因此才需要特定规则的命名格式),而使用第二种方式的RegisterNatives方法将本地函数向虚拟机进行登记,可以让其更有效率的找到函数;
  2. 在运行时动态调整本地函数与Java函数值之间的映射关系,只需要多次调用RegisterNatives()方法,并传入不同的映射表参数。例如以上中使用static JNINativeMethod methods[] = { { "encrypt", "([B)[B", (jbyteArray *) encrypt} }的方式实现多个native方法的映射。

到这里基本可以得出之所以Thread、Classload源码中使用第二种方式快是因为JNI中的registerNatives()方法使程序主动将本地方法链接到调用方,当Java程序需要调用本地方法时就可以直接调用,而不需要虚拟机再去定位并链接。为了更清晰的认识是如何注册下面需要阅读C/C++的源码。

4. registerNatives()源码

在Object、Thread等类中通过registerNatives将指定的本地方法绑定到指定函数,如将hashCode和clone本地方法绑定到JVM_IHashCode和JVM_IHashCode函数。

以下为代码OpenJDK中的Thread.c的部分代码

···
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}
};

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
···
复制代码

通过以上代码可知Java中的registerNatives代码的目的就是注册绑定本地方法,其方式是通过JNI_onload函数实现动态绑定。

备注:

  1. JNINativeMethod结构体
typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;
复制代码

  以上代码为jni.h中的源码,可见 JNINativeMethod 包含三个元素:方法名,方法签名,native函数指针,该结构体用于描述需要注册的方法信息。

2. JNI_OnLoad和JNI_OnUnload函数

JNI_OnLoad():函数在VM执行System.loadLibrary(xxx)函数时被调用,有两个重要的作用:指定JNI版本;告诉VM该组件使用那一个JNI版本(若未提供JNI_OnLoad()函数,VM会默认该使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI1.4版,则必须由JNI_OnLoad()函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中) 来通知虚拟机初始化设定,当虚拟机执行到System.loadLibrary()函数时,会立即执行JNI_OnLoad()方法,在该方法中进行各种资源的初始化操作最为恰当,RegisterNatives此时这里进行。

JNI_OnUnload():是当VM释放该组件时被调用,JNI_OnUnload()函数的作用与JNI_OnLoad()对应,因此在该方法中进行善后清理,资源释放的动作最为合适。

4. JNI中的RegisterNatives()方法是JNI中提供用来注册Native方法的方法,该方法的声明在一个JNINativeInterface_结构体中.

5. JNIEnv类型实际上代表了Java环境,通过这个JNIEnv*指针,可以对Java端的代码进行操作。例如创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作 。

6. 上面代码示例实在Eclipse+Visual Studio 2015上运行的,运行代码的时候需要注意环境的配置。

原文  https://juejin.im/post/5dbb959151882523b5402c8f
正文到此结束
Loading...