Java呼叫本地方法又是怎麼一回事

超人汪小建發表於2019-03-04

JNI

JNI即Java Native Interface,它能在Java層實現對本地方法的呼叫,一般本地的實現語言主要是C/C++,其實從虛擬機器層面來看JNI挺好理解,JVM主要使用C/C++ 和少量彙編編寫,在執行Java位元組碼時如果遇到有某個方法標明為Native的則從JVM中找到對應的C/C++函式,一般本地方法對應的函式會被註冊到JVM中。

使用JNI能讓Java與本地語言互動,但一般也意味著喪失了跨平臺性,而有些場合會使用。比如標準的Java特性不符合你的需求時,比如在效能要求很高的某段邏輯。

從一個例子說起

  • 編寫一個Java類提供本地加密的方法,其中加密方法為本地方法,實現是在ByteCodeEncryptor動態庫。
package com.seaboat.bytecode;

public class ByteCodeEncryptor {
  static{
    System.loadLibrary("ByteCodeEncryptor"); 
  }

  public native static byte[] encrypt(byte[] text);

}複製程式碼
  • 為方便起見,不自己寫標頭檔案,用javah -jni com.seaboat.bytecode.ByteCodeEncryptor生成。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_seaboat_bytecode_ByteCodeEncryptor */

#ifndef _Included_com_seaboat_bytecode_ByteCodeEncryptor
#define _Included_com_seaboat_bytecode_ByteCodeEncryptor
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_seaboat_bytecode_ByteCodeEncryptor
 * Method:    encrypt
 * Signature: ([B)[B
 */
JNIEXPORT jbyteArray JNICALL Java_com_seaboat_bytecode_ByteCodeEncryptor_encrypt
  (JNIEnv *, jclass, jbyteArray);

#ifdef __cplusplus
}
#endif
#endif複製程式碼
  • 編寫原始檔,實現標頭檔案宣告的函式。
#include "com_seaboat_bytecode_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_seaboat_bytecode_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;
}複製程式碼
  • 用cl進行編譯,生成動態庫,指定編譯需要的一些標頭檔案。
cl /EHsc -ID:Javajdk1.8.0_73include -ID:Javajdk1.8.0_73includewin32 -LD com_seaboat_bytecode_ByteCodeEncryptor.cpp -FeByteCodeEncryptor.dll複製程式碼
  • 可以呼叫Java層的ByteCodeEncryptor類的encrypt方法了。

怎麼載入動態庫

Java層需要呼叫System.loadLibrary去載入動態庫,而它其實就是通過ClassLoaderloadLibrary方法來載入,載入的大致邏輯為:

  1. 是不是使用了絕對路徑來指定動態庫,如果是則直接通過絕對路徑來載入。
  2. 如果啟動Java時帶有-Dsun.boot.library.path=xxxx時,則去改引數指定的目錄下尋找動態庫。
  3. 如果啟動Java時帶有-Djava.library.path=xxxx時,則去改引數指定的目錄下尋找動態庫。
    載入動態庫在Java層面實現不了,所以必須會通過本地才能真正實現載入操作,Java層面最後是走到NativeLibrary類,其包含的load本地方法為真正的載入註冊操作。

對應著ClassLoader.cJava_java_lang_ClassLoader_00024NativeLibrary_load函式,因為NativeLibrary在Java層的ClassLoader的子類,所以其中包含一串數字00024,即表示美元符號。該函式最重要的一步是調了JVM_LoadLibrary函式,該函式如下,核心的一步是os::dll_load,它會根據不同的作業系統做不同的處理。

JVM_ENTRY_NO_ENV(void*, JVM_LoadLibrary(const char* name))
  //%note jvm_ct
  JVMWrapper2("JVM_LoadLibrary (%s)", name);
  char ebuf[1024];
  void *load_result;
  {
    ThreadToNativeFromVM ttnfvm(thread);
    load_result = os::dll_load(name, ebuf, sizeof ebuf);
  }
  if (load_result == NULL) {
    char msg[1024];
    jio_snprintf(msg, sizeof msg, "%s: %s", name, ebuf);
    // Since `ebuf` may contain a string encoded using
    // platform encoding scheme, we need to pass
    // Exceptions::unsafe_to_utf8 to the new_exception method
    // as the last argument. See bug 6367357.
    Handle h_exception =
      Exceptions::new_exception(thread,
                                vmSymbols::java_lang_UnsatisfiedLinkError(),
                                msg, Exceptions::unsafe_to_utf8);

    THROW_HANDLE_0(h_exception);
  }
  return load_result;
JVM_END複製程式碼

看一個圖,它包含了linuxsolariswindows三大型別作業系統的處理,下面分別看看不同作業系統如何處理。

  • 對於linux,主要通過dlopen函式來開啟動態庫,並載入到記憶體中,再通過dlsym函式可以獲取動態庫中的函式指標,於是就能實現呼叫動態庫某函式。
  • 對於solaris,主要通過dlopen函式來開啟動態庫,並載入到記憶體中,再通過dlsym函式可以獲取動態庫中的函式指標,但它與linux不同的是dlsym在linux中是非執行緒安全的,需要加鎖,而solaris則不需要。
  • 對於windows,主要通過LoadLibrary函式載入動態庫,載入到記憶體中,再通過GetProcAddress函式可以獲取動態庫的函式指標,從而實現呼叫動態庫某函式。

另外,我們注意到Java層不必指定動態庫的字尾,這個留給JVM去解決,它會根據不同作業系統新增不同的字尾,這個邏輯由System.cJava_java_lang_System_mapLibraryName函式實現,它會有如下兩個字尾。

#define JNI_LIB_SUFFIX ".so"

#define JNI_LIB_SUFFIX ".dll"複製程式碼

位元組碼

對於位元組碼,它是Java執行時的指令,其實想一下就能想到本地方法要在執行時區別於Java層的呼叫,所以必須要有一個flag來標識本地方法,那我們們用javap來看看上面包含本地方法的class會有什麼標識,可以看到存在一個ACC_NATIVE,有了它就可以在執行時呼叫C/C++函式了。

public static native byte[] encrypt(byte[]);
    descriptor: ([B)[B
    flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE複製程式碼

總結一下

兩句話總結起來就是,Java編譯器將包含本地方法的class對應的方法新增ACC_NATIVE標識,而JVM負責將動態庫載入到記憶體,Java執行引擎執行到本地方法時找到對應的函式,完成本地方法的呼叫。

以下是廣告相關閱讀

========廣告時間========

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

相關閱讀:
註解的原理又是怎麼一回事

歡迎關注:

相關文章