雖然Java虛擬機器為開發人員遮蔽了底層的實現細節,使得開發人員不用考慮底層作業系統的差異性。不過在某些應用程式中,還是免不了要直接與底層作業系統上的原生程式碼進行互動。今天我們就來看一下Java對本地呼叫提供的支援。
一、為什麼要進行本地呼叫
1.基於效能的考慮
Java語言從其執行速度上來說,在大多數方面是慢於底層作業系統上原生的C和C++等語言的。這主要是由於Java虛擬機器這個中間層次的存在。如果完全用Java語言實現的效能無法達到程式的預期要求,可以選擇把部分重要且耗時的程式碼用C或C++來實現。
2.基於某些特殊的需求
Java平臺提供的標準類庫的功能很強大,包括了在開發中可能遇到的大部分功能。但是仍然有一些功能無法用標準API來實現,主要是一些與底層硬體平臺直接互動的功能。Java虛擬機器沒有把這一部分功能暴露給執行在其上的程式。如果需要這方面的功能,那麼只能使用原生程式碼來進行開發。
3.與已有的使用原生程式碼編寫的程式之間進行整合。
如果Java程式需要與底層作業系統上由C和C++語言開發的程式進行互動,那麼可以進行本地呼叫。
我們平時的開發更多的情況是後邊兩種情況;在elasticsearch中基本上是屬於第二種情況。
二、使用JNI實現本地呼叫
針對以上提到的各種情況,Java提供了JNI(Java Native Interface)和JNA(Java Native Access)兩種方式,其中JNI的一個重要使用場景是提高程式的效能。當對程式中關鍵部分的效能要求比較高的時候,可以使用C和C++程式碼來實現。
我們先來看下怎麼使用JNI來進行本地呼叫。
首先我們需要有一個Java類來宣告本地方法,並負責載入原生程式碼庫。本地方法與Java介面中的方法或抽象類中的抽象方法一樣,只包含方法宣告,沒有相關的實現。程式中的其他部分可以用正常的方法呼叫本地方法,比如引數傳遞和返回值使用等都與正常的方法相同。當虛擬機器在執行本地方法時,會嘗試在已經載入的原生程式碼庫中查詢本地方法的對應實現。在查詢到對應的實現方法之後,虛擬機器會負責進行引數傳遞、實際方法呼叫和返回值傳遞等工作。
public class HelloNative {
static{
System.loadLibrary("greetLib");
}
public static native void greeting();
}
下一步要編寫實現本地方法的C/C++程式碼。Java提供的命令列工具根據Java原始碼生成C/C++程式碼所需的標頭檔案。對於本地方法,標頭檔案中會包含相關的方法宣告與其對應。
F:\source\JNI\src>javac -h .\ .\HelloNative.java
通過下邊自動生成的標頭檔案,我們可以看到這裡有很多的隱式約定,我們只要按照這個宣告進行實現即可,具體的規則不是今天的重點,不進行詳述。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloNative */
#ifndef _Included_HelloNative
#define _Included_HelloNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloNative
* Method: greeting
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloNative_greeting
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
三、elasticsearch使用JNA實現本地呼叫
通過上邊對JNI的簡單瞭解,我們更多的時候碰到的情況是,在編寫Java程式之前,就已經有了可以使用的原生程式碼庫。這個原生程式碼庫可能是程式的一部分,也可能是底層作業系統自帶的。這些原生程式碼庫的特點是在實現的時候並沒有考慮與Java虛擬機器的整合,因此也沒有使用與JNI相關的內容。在使用這樣的原生程式碼庫時,我們就需要一箇中間的原生程式碼庫作為橋樑。這個原生程式碼庫作為Java程式中本地方法的實現,負責實際呼叫時的引數型別轉換和返回值傳遞等工作。這個過程是十分的繁瑣的,Java提供了JNA來支援這種情況。
我們知道elasticsearch啟動的時候需要檢測當前使用者是否是root使用者,這個檢測是直接呼叫的底層作業系統的程式碼,我們來看下elasticsearch是怎樣使用JNA實現的。
首先elasticsearch提供了Natives類,作為呼叫本地方法的入口,並負責檢測JNA的可用性。
static {
boolean v = false;
try {
// load one of the main JNA classes to see if the classes are available. this does not ensure that all native
// libraries are available, only the ones necessary by JNA to function
Class.forName("com.sun.jna.Native");
v = true;
} catch (ClassNotFoundException e) {
logger.warn("JNA not found. native methods will be disabled.", e);
} catch (UnsatisfiedLinkError e) {
logger.warn("unable to load JNA native support library, native methods will be disabled.", e);
}
JNA_AVAILABLE = v;
}
檢測JNA是否可用,然後再呼叫JNANatives的對用方法
static boolean definitelyRunningAsRoot() {
if (!JNA_AVAILABLE) {
logger.warn("cannot check if running as root because JNA is not available");
return false;
}
return JNANatives.definitelyRunningAsRoot();
}
在JNANatives的definitelyRunningAsRoot中,如果是非windows系統,則呼叫
JNACLibrary.geteuid
/** Returns true if user is root, false if not, or if we don't know */
static boolean definitelyRunningAsRoot() {
if (Constants.WINDOWS) {
return false; // don't know
}
try {
return JNACLibrary.geteuid() == 0;
} catch (UnsatisfiedLinkError e) {
// this will have already been logged by Kernel32Library, no need to repeat it
return false;
}
}
elasticsearch使用JNAKernel32Library來封裝對windows的Kernel32的呼叫,使用 JNACLibrary來封裝對非windows系統的libc的呼叫
static {
try {
Native.register("c");
} catch (UnsatisfiedLinkError e) {
logger.warn("unable to link C library. native methods (mlockall) will be disabled.", e);
}
}
static native int mlockall(int flags);
static native int geteuid();
四、總結
- JNI更適合使用本地呼叫來解決對效能有更高要求的場景,需要我們自己使用C或者C++來實現處理邏輯。
- 對於呼叫已有的本地庫的方法或者作業系統的方法,使用JNA更為方便便捷。