1. 程式人生 > 程式設計 >Java之Thread原始碼registerNatives()深入理解

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實現步驟

  1. 在Java中宣告native()方法,然後編譯;
  2. 使用javah命令生成.h檔案;
  3. 編寫.cpp檔案實現native匯出方法,其中需要包含第二步產生的.h檔案(注意其中需包含JDK帶的jni.h檔案);
  4. 將原生程式碼編譯成動態庫(Windows:*.dll,linux/unix:*.so,mac os x:*.jnilib);
  5. Java中使用System.loadLibrary()方法載入第四步產生的動態連結庫檔案,至此,整個過程結束。

這裡只是簡單的介紹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,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,(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",(void *)&JVM_SuspendThread},{"resume0",(void *)&JVM_ResumeThread},{"setPriority0","(I)V",(void *)&JVM_SetThreadPriority},{"yield",(void *)&JVM_Yield}
};

JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env,jclass cls)
{
    (*env)->RegisterNatives(env,cls,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上執行的,執行程式碼的時候需要注意環境的配置。

5. 參考文獻

  1. JNI 學習筆記——通過RegisterNatives註冊原生方法
  2. Object類中的registerNatives方法的作用深入介紹
  3. 呼叫jni的兩種方法javah和RegisterNatives
  4. 使用RegisterNatives註冊原生程式碼

最後,感謝以上作者能夠分享這麼好的文章讓我學習。