Java調用本地方法又是怎麼一回事

JNI

JNI即Java Native Interface,它能在Java層實現對本地方法的調用,通常本地的實現語言主要是C/C++,其實從虛擬機層面來看JNI挺好理解,JVM主要使用C/C++ 和少許彙編編寫,在執行Java字節碼時若是遇到有某個方法標明爲Native的則從JVM中找到對應的C/C++函數,通常本地方法對應的函數會被註冊到JVM中。html

使用JNI能讓Java與本地語言交互,但通常也意味着喪失了跨平臺性,而有些場合會使用。好比標準的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:\Java\jdk1.8.0_73\include\ -ID:\Java\jdk1.8.0_73\include\win32 -LD com_seaboat_bytecode_ByteCodeEncryptor.cpp -FeByteCodeEncryptor.dll複製代碼
  • 能夠調用Java層的ByteCodeEncryptor類的encrypt方法了。

怎麼加載動態庫

Java層須要調用System.loadLibrary去加載動態庫,而它其實就是經過ClassLoaderloadLibrary方法來加載,加載的大體邏輯爲:linux

  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,它會根據不一樣的操做系統作不一樣的處理。windows

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函數實現,它會有以下兩個後綴。bash

#define JNI_LIB_SUFFIX ".so"

#define JNI_LIB_SUFFIX ".dll"複製代碼

字節碼

對於字節碼,它是Java執行時的指令,其實想一下就能想到本地方法要在執行時區別於Java層的調用,因此必需要有一個flag來標識本地方法,那我們用javap來看看上面包含本地方法的class會有什麼標識,能夠看到存在一個ACC_NATIVE,有了它就能夠在執行時調用C/C++函數了。app

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

總結一下

兩句話總結起來就是,Java編譯器將包含本地方法的class對應的方法添加ACC_NATIVE標識,而JVM負責將動態庫加載到內存,Java執行引擎執行到本地方法時找到對應的函數,完成本地方法的調用。jvm

如下是廣告相關閱讀函數

========廣告時間========性能

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

爲何寫《Tomcat內核設計剖析》

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

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

歡迎關注:

相關文章
相關標籤/搜索