Native
- 所謂的native準確的說是藉由虛擬機器實現的JNI介面呼叫的作業系統提供的API
- JNI使得class中的ACC_NATIVE標至的方法能借由JNI類的例項轉換為JNI規範(如全限定名)的c實現方法例項(已經由.lib在虛擬機器初始化時載入或者藉由已經載入的類庫的load方法,用java等語言加入記憶體),該例項會呼叫本地方法棧中的方法(作業系統提供的API)
.h、.cpp、.lib和.dll
.h標頭檔案和.cpp是編譯時必須的,lib是連結時需要的,dll是執行時需要的。
.h:宣告函式介面
.cpp:c++語言實現的功能原始碼
.lib :
LIB有兩種,一種是靜態庫,比如C-Runtime庫,這種LIB中有函式的實現程式碼,一般用在靜態連編上,它是將LIB中的程式碼加入目標模組(EXE或者DLL)檔案中,所以連結好了之後,LIB檔案就沒有用了。
一種LIB是和DLL配合使用的,裡面沒有程式碼,程式碼在DLL中,這種LIB是用在靜態呼叫DLL上的,所以起的作用也是連結作用,連結完成了,LIB也沒用了。至於動態呼叫DLL的話,根本用不上LIB檔案。 目標模組(EXE或者DLL)檔案生成之後,就用不著LIB檔案了。
.dll:
動態連結庫英文為DLL,是Dynamic Link Library的縮寫。DLL是一個包含可由多個程式,同時使用的程式碼和資料的庫。
當程式使用 DLL 時,具有以下的優點: 使用較少的資源,當多個程式使用同一個函式庫時,DLL 可以減少在磁碟和實體記憶體中載入的程式碼的重複量(執行時需要的庫是需要加入記憶體的)。
.h和.cpp編譯後會生成.lib和.dll 或者 .dll 檔案
我們的程式引用別的檔案的函式,需要呼叫其標頭檔案,但是標頭檔案找到相應的實現有兩種方式,一種是同個專案目錄下的其他cpp檔案(公用性差),一種是連結時的lib檔案(靜態,lib中自己有實現程式碼),一種是執行時的dll檔案,一種是lib和dll 的結合(動態,lib放索引,dll為具體實現)
還要指定編譯器連結相應的庫檔案。在IDE環境下,一般是一次指定所有用到的庫檔案,編譯器自己尋找每個模組需要的庫;在命令列編譯環境下,需要指定每個模組呼叫的庫。
一般不開源的系統是後面三種方式,因為可以做到介面開放,原始碼閉合
靜態連結庫
靜態連結庫(Static Libary,以下簡稱“靜態庫”),靜態庫是一個或者多個obj檔案的打包,所以有人乾脆把從obj檔案生成lib的過程稱為Archive,即合併到一起。比如你連結一個靜態庫,如果其中有錯,它會準確的找到是哪個obj有錯,即靜態lib只是殼子,但是靜態庫本身就包含了實際執行程式碼、符號表等等。
如果採用靜態連結庫,在連結的時候會將lib連結到目的碼中,結果便是lib 中的指令都全部被直接包含在最終生成的 EXE 檔案中了。
這個lib檔案是靜態編譯出來的,索引和實現都在其中。
靜態編譯的lib檔案有好處:給使用者安裝時就不需要再掛動態庫了。但也有缺點,就是導致應用程式比較大,而且失去了動態庫的靈活性,在版本升級時,同時要釋出新的應用程式才行。
動態連結庫(DLL)
.dll + .lib : 匯入庫形式,在動態庫的情況下,有兩個檔案,而一個是引入庫(.LIB)檔案,一個是DLL檔案,引入庫檔案包含被DLL匯出的函式的名稱和位置,DLL包含實際的函式和資料,應用程式使用LIB檔案連結到所需要使用的DLL檔案,庫中的函式和資料並不複製到可執行檔案中,因此在應用程式的可執行檔案中,存放的不是被呼叫的函式程式碼,而是DLL中所要呼叫的函式的記憶體地址,這樣當一個或多個應用程式執行是再把程式程式碼和被呼叫的函式程式碼連結起來,從而節省了記憶體資源。
從上面的說明可以看出,DLL和.LIB檔案必須隨應用程式一起發行,否則應用程式將會產生錯誤。
.dll形式: 單獨的可執行檔案形式,因為沒有lib 的靜態載入,需要自己手動載入,LoadLibary調入DLL檔案,然後再手工GetProcAddress獲得對應函式了,若是java 會呼叫System的LoadLibary,但是也是呼叫JVM中對於作業系統的介面,使用作業系統的LoadLibary等方法真正的將.dll讀入記憶體,再呼叫生成的相應函式。
.dll+ .lib和.dll本質上是一樣的,只是前者一般用於通用庫的預設定,是的我們通過lib直接能查詢到.dll檔案,不用我們自己去查詢,雖會消耗一部分效能,但是實用性很大。.dll 每一個需要到的檔案都需自己呼叫載入命令,容易出錯與浪費較多時間(但是我們測試時卻可以很快的看出功能實現情況,而且更靈活地呼叫)
JNI
JNI是Java Native Interface的縮寫,通過使用 Java本地介面書寫程式,可以確保程式碼在不同的平臺上方便移植,它允許Java程式碼和其他語言寫的程式碼進行互動。
java生成符合JNI規範的C介面檔案(標頭檔案):
-
編寫帶有native宣告的方法的java類
-
使用javac命令編譯所編寫的java類
-
然後使用javah + java類名生成副檔名為h的標頭檔案
-
使用C/C++實現本地方法
-
將C/C++編寫的檔案生成動態連線庫 (linux gcc windows 可以用VS)
編寫範例:https://blog.csdn.net/wzgbgz/article/details/82979728
生成的.h的樣例:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class NativeDemo */
#ifndef _Included_NativeDemo
#define _Included_NativeDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: NativeDemo
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_NativeDemo_sayHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
“jni.h” 是必須要匯入的,因為JNIEXPORT等都需要他的支援才行,而且有些方法中需要藉助裡面的函式。
Java_NativeDemo_sayHello這樣的規範命名是生成的.dll在被作業系統dlopen讀取入記憶體時返回的handle能經由dlsym擷取出正確的函式名,他可能將xxx.dll全都載入入記憶體,放入一個handle或者一個handle集合中,這時就需要包的全限定類名來確定到底獲取的是handle中的哪個方法了
JNIEnv ,jobject ,jclass
1. JNIEnv類實際代表了Java環境,通過這個JNIEnv 指標,就可以對Java端的程式碼進行操作。例如,建立Java類的物件,呼叫Java物件的方法,獲取Java物件的屬性等等,JNIEnv的指標會被JNI傳入到本地方法的實現兩數中來對Java端的程式碼進行操作。
JNIEnv類中有很多函式用可以用如下所示其中:TYPE代表屬性或者方法的型別(比如:int float double byte ......)
1.NewObject/NewString/New<TYPE>Array
2.Get/Set<TYPE>Field
3.Get/SetStatic<TYPE>Field
4.Call<TYPE>Method/CallStatic<TYPE>Method等許許多多的函式
2. jobject代表了在java端呼叫本地c/c++程式碼的那個類的一個例項(物件)。在修改和呼叫java端的屬性和方法的時候,用jobject 作為引數,代表了修改了jobject所對應的java端的物件的屬性和方法
3. jclass : 為了能夠在c/c++中使用java類,JNI.h標頭檔案中專門定義了jclass型別來表示java中的Class類
JNIEvn中規定可以用以下幾個函式來取得jclass
1.jclass FindClass(const char* clsName) ;
2.jclass GetObjectClass(jobject obj);
3.jclass GetSuperClass(jclass obj);
JNI原理
我們編譯xxx.h和xxx.cpp生成了dll檔案,執行java檔案JNI會幫我們呼叫dll中的方法, 但是java物件是如何具體呼叫他的我們不清楚
我們自己實現的dll需要大概如下的模板:
Test.java
package hackooo;
public class Test{
static{
// java層呼叫.dll檔案進入記憶體,但是底層仍是由虛擬機器呼叫JNI用C實現對作業系統的提供的介面載入入記憶體
System.loadLibrary("bridge");
}
public native int nativeAdd(int x,int y);
public int add(int x,int y){
return x+y;
}
public static void main(String[] args){
Test obj = new Test();
System.out.printf("%d\n",obj.nativeAdd(2012,3));
System.out.printf("%d\n",obj.add(2012,3));
}
}
我們需要先看到System.loadLibrary("bridge")的作用
@CallerSensitive
public static void loadLibrary(String libname) {
// Runtime類是Application程式的建立後,用來檢視JVM當前狀態和控制JVM行為的類
// Runtime是單例模式,且只能用靜態getRuntime獲取,不能例項化
// 其中load是載入動態連結庫的絕對路徑方法
// loadLibrary是讀取相對路徑的,動態連結庫需要在java.library.path中,一般為系統path,也可以設定啟動項的 -VMoption
// 通過ClassLoader.loadLibrary0(fromClass, filename, true);中的第三個引數判斷
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
java.lang.Runtime
@CallerSensitive
public void loadLibrary(String libname) {
loadLibrary0(Reflection.getCallerClass(), libname);
}
synchronized void loadLibrary0(Class<?> fromClass, String libname) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkLink(libname);
}
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
// false,呼叫相對路徑
ClassLoader.loadLibrary(fromClass, libname, false);
}
java.lang.ClassLoader
static void loadLibrary(Class<?> fromClass, String name,
boolean isAbsolute) {
// 通過方法區中的class類找到相應的類載入器
ClassLoader loader =
(fromClass == null) ? null : fromClass.getClassLoader();
if (sys_paths == null) {
// 載入的絕對路徑
// 系統環境變數
usr_paths = initializePath("java.library.path");
// 我們啟動時加入的依賴項
sys_paths = initializePath("sun.boot.library.path");
}
if (isAbsolute) {
// 若是決定路徑,呼叫真正的執行方法
if (loadLibrary0(fromClass, new File(name))) {
return;
}
throw new UnsatisfiedLinkError("Can't load library: " + name);
}
if (loader != null) {
// 判斷當前類載入器及其雙親是否有該lib的類資訊
String libfilename = loader.findLibrary(name);
if (libfilename != null) {
File libfile = new File(libfilename);
if (!libfile.isAbsolute()) {
throw new UnsatisfiedLinkError(
"ClassLoader.findLibrary failed to return an absolute path: " + libfilename);
}
if (loadLibrary0(fromClass, libfile)) {
return;
}
throw new UnsatisfiedLinkError("Can't load " + libfilename);
}
}
// 查詢sys_paths路徑下是否有.dll檔案
for (int i = 0 ; i < sys_paths.length ; i++) {
File libfile = new File(sys_paths[i], System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile)) {
return;
}
libfile = ClassLoaderHelper.mapAlternativeName(libfile);
if (libfile != null && loadLibrary0(fromClass, libfile)) {
return;
}
}
// 查詢usr_paths路徑下是否有.dll檔案
if (loader != null) {
for (int i = 0 ; i < usr_paths.length ; i++) {
File libfile = new File(usr_paths[i],
System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile)) {
return;
}
libfile = ClassLoaderHelper.mapAlternativeName(libfile);
if (libfile != null && loadLibrary0(fromClass, libfile)) {
return;
}
}
}
// Oops, it failed
throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
}
private static boolean loadLibrary0(Class<?> fromClass, final File file) {
// Check to see if we're attempting to access a static library
// 檢視是否呼叫的lib為靜態連結庫
String name = findBuiltinLib(file.getName());
boolean isBuiltin = (name != null);
// 若是靜態連結庫則跳過,否則獲取file的路徑
if (!isBuiltin) {
boolean exists = AccessController.doPrivileged(
new PrivilegedAction<Object>() {
public Object run() {
return file.exists() ? Boolean.TRUE : null;
}})
!= null;
if (!exists) {
return false;
}
try {
name = file.getCanonicalPath();
} catch (IOException e) {
return false;
}
}
ClassLoader loader =
(fromClass == null) ? null : fromClass.getClassLoader();
//
Vector<NativeLibrary> libs =
loader != null ? loader.nativeLibraries : systemNativeLibraries;
synchronized (libs) {
int size = libs.size();
for (int i = 0; i < size; i++) {
NativeLibrary lib = libs.elementAt(i);
if (name.equals(lib.name)) {
return true;
}
}
synchronized (loadedLibraryNames) {
if (loadedLibraryNames.contains(name)) {
throw new UnsatisfiedLinkError
("Native Library " +
name +
" already loaded in another classloader");
}
/* If the library is being loaded (must be by the same thread,
* because Runtime.load and Runtime.loadLibrary are
* synchronous). The reason is can occur is that the JNI_OnLoad
* function can cause another loadLibrary invocation.
*
* Thus we can use a static stack to hold the list of libraries
* we are loading.
*
* If there is a pending load operation for the library, we
* immediately return success; otherwise, we raise
* UnsatisfiedLinkError.
*/
//如果我們突然發現library已經被載入,可能是我們執行一半被掛起了或者其他執行緒在synchronized前也呼叫了該classLoader,執行JNI_OnLoad又一次呼叫了啟用了同個執行緒中過的另一個loadLibrary方法,載入了我們的檔案
//之所以是同個執行緒中的,因為run一個application對應一個java.exe/javaw.extin程式,一個JVM例項,一個Runtime例項,且其是實現了synchronized的。
// 檢視此時nativeLibraryContext中儲存了什麼
int n = nativeLibraryContext.size();
for (int i = 0; i < n; i++) {
NativeLibrary lib = nativeLibraryContext.elementAt(i);
if (name.equals(lib.name)) {
if (loader == lib.fromClass.getClassLoader()) {
return true;
} else {
throw new UnsatisfiedLinkError
("Native Library " +
name +
" is being loaded in another classloader");
}
}
}
NativeLibrary lib = new NativeLibrary(fromClass, name, isBuiltin);
nativeLibraryContext.push(lib);
try {
// 嘗試載入
lib.load(name, isBuiltin);
} finally {
nativeLibraryContext.pop();
}
if (lib.loaded) {
// 加入已載入Vetor中
loadedLibraryNames.addElement(name);
libs.addElement(lib);
return true;
}
return false;
}
}
}
native void load(String name, boolean isBuiltin);
最後的load是虛擬機器中實現的方法(用來載入我們自己要加入的.dll的),我們通過呼叫他來呼叫作業系統的API來真正將其放入記憶體
而那些已經編譯好的庫函式,虛擬機器初始化時就呼叫LoadLibrary(Linux是dlopen)等作業系統API(本地方法棧)加入了記憶體中
(windows的)LoadLibrary與dlopen原理相似,若是還未載入過的dll,會呼叫相關方法,windows會用DLL_PROCESS_ATTACH呼叫DllMain 方法,若是成功則返回一個handle物件可以呼叫GetProcAddress(linux 為dlsym)獲得函式進行使用。
load是在jVM初始化就載入了lib檔案,通過jvm.h就能通過該lib找到呼叫的函式的入口,呼叫相應的.dll二進位制檔案
LoadLibrary是作業系統初始化時載入的windows.lib載入入記憶體的,我們需要呼叫windows.h檔案,呼叫該函式的.dll入記憶體(延遲載入的話)
我們java中的native方法的實現和到此時load便接軌了,我們來看看native如何被解析的
編譯:
javac hackooo/Test.java
javap -verbose hackooo.Test
Test.class:
public native int nativeAdd(int, int);
flags: ACC_PUBLIC, ACC_NATIVE
public int add(int, int);
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 8: 0
普通的“add”方法是直接把位元組碼放到code屬性表中,而native方法,與普通的方法通過一個標誌“ACC_NATIVE”區分開來。java在執行普通的方法呼叫的時候,可以通過找方法表,再找到相應的code屬性表,最終解釋執行程式碼,那麼,對於native方法,在class檔案中,並沒有體現native程式碼在哪裡,只有一個“ACC_NATIVE”的標識,那麼在執行的時候改怎麼找到動態連結庫的程式碼呢?
到了這一步,我們就需要開始鑽研JVM到底執行邏輯是什麼了
剛開始時,我們通過javac 編譯一個xxx.java 成一個位元組碼檔案,javac進行前端編譯時包括了詞法分析,語法分析生成抽象語法樹,在生成位元組碼指令流(編譯期)後交由直譯器/即時編譯器進行解釋/編譯優化(執行期)
然後用java xxx 命令在作業系統中初始化一個程式,這個程式為我們分配了一塊記憶體空間,我們開始新建一個JVM(或者說是JRE)在該記憶體中並進行初始化(該步驟是作業系統通過java這個命令(其為windows的一個指令碼),呼叫其他系統命令將我們預先編譯好的二進位制指令集放入CPU執行生成)
虛擬機器的例項建立好後,java指令碼的最後一條命令便是執行JVM中的main方法,jvm會幫我們建立BoostrapClassLoader,其是用C實現的,並不符合加入class區後的例項化流程,因此我們的java程式碼並不能引用他,建立完他後,BoostrapClassLoader會幫我們將一些jdk的核心class檔案通過它載入入方法區中,緊接著JVM會通過launcher的c實現通過JNI(還需看原始碼確定是不是這樣,JNI是JVM初始化時建立的?不在JVM執行時區域中,在執行引擎中),依據匯入java實現的Launcher的class資訊通過幫我們建立sun.misc.Launcher物件並初始化(單例),他的建立還會伴隨著ExtClassLoader的初始化和appClassLoader的建立(三層和雙親),這裡涉及類的載入過程.
更好的瞭解java實現的ClassLoaderhttps://blog.csdn.net/briblue/article/details/54973413
接著,執行緒會預設呼叫APPClassLoader幫我們將命令中的 xxx引數的class裝入方法區(之所以要通過classLoader來載入是為了只在需要時我們載入類,而不是全部載入,節約記憶體空間,而這裡載入的class不止硬碟,只要是二進位制位元組流就可以),併為main函式在java棧中預留一個棧幀,經生成的後端編譯器的例項進行位元組碼的解釋執行優化和編譯優化代替執行(後端編譯器大部分既有直譯器又有編譯器引數設定,決定如何編譯優化).
從APPClassLader將class裝入方法區開始,就是類的載入過程了
具體流程是
-
載入(既可以由JVM本身載入入方法區,也可自定義的classLoder選取需要載入的class,通過JNI呼叫)
通過一個類的全限定類名來獲取定義此類的二進位制位元組流
將這個位元組流所代表的靜態結構轉化為方法區的執行時資料結構
在記憶體(堆)中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口(單例模式)
至於什麼時候載入,除了遇到new、getstatic、putstatic、invokestatic四條指令時,必須立刻載入··到初始化完成
-
驗證(java原始碼本身的編譯是相對安全的,但是位元組碼的內容可能會加入惡意程式碼,因此需要驗證)
檔案格式驗證(位元組流的各個部分劃分是否符合規範)
後設資料驗證(對後設資料資訊中的資料型別檢驗)
位元組碼校驗(對方法體中的內容進行校驗,較為複雜耗時,jdk6後可以將權重部分移向了javac)
符號引用校驗(在解析階段同時進行)
-
準備
正式為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設定類變數初始值的階段。從概念上講,這些變數所使用的記憶體都應當在方法區中進行分配,但需要注意的是方法區本身是一個邏輯層面的概念,其實現在不同的版本,不同的虛擬機器上可能分佈在不同的記憶體空間,如同JMM之於JVM一般
jdk 8之前,HotSpot團隊選擇把收集器的分代擴充套件至方法區,由垃圾收集器統一收集,省去專門寫一個獨立的管理方法區的方法,而方法區的儲存內容與前面的分代的更新換代條件大不相同,所以專門劃分了個永久代,但這容易導致更多的記憶體溢位問題
jdk6hotspot就將捨棄永久代放進了發展策略,逐步改用成了用直接記憶體(Direct Memory)中的元空間等來儲存方法區的內容,實現單獨的回收管理,
jdk7已經將字串常量池、靜態變數等移出,jdk8以全部移出
jdk8 時類變數會隨著Class物件一起存放到Java堆中,型別資訊則放到了直接記憶體中了。
圖網上找的(其中類資訊也稱為靜態常量池)
-
解析
解析階段是java虛擬機器將常量池內的符號引用(存放在方法區的常量池中)替換為直接引用(我們當初在堆中建立的Class物件的具體記憶體地址)的過程,即將我們最初的ACC_NATIVE等字面量進替換。
載入階段只是將位元組碼按表靜態翻譯成位元組碼對應的表示按約定大小劃分入記憶體中,常量池中只存放字面量並被翻譯的方法表中的方法引用作為所儲存記憶體的部分資訊儲存,只有在解析階段才專門將常量池中的字元引用依據Class物件中分出的各個記憶體中預先儲存的部分資訊匹配返回地址換成直接引用。放入執行時常量池直接呼叫
- 至jdk13常量池中存有 17類常量表,每一個tag用u1長度(兩個位元組)代表一類常量表,對應的常量表中規定了後面需要讀取多少位元組分別,分為幾個部分代表哪些東西。
我們需要了解一份class檔案大概有哪些資訊(xx資訊便是xx表集合)
解析可以發生在任何時間,包括執行時再被確定也是可能的,只要求了在執行anewarray,checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, 等17個用於操作符號引用的位元組碼指令之前,需要對他們所使用的符號引用進行解析
符號引用可以將第一次的解析結果進行快取,如在執行時直接引用常量池中的記錄。不過對於invokedynamic指令,上面的規則就不使用了,它要求程式在直譯器基於棧或者編譯器基於暫存器解讀方法時實際執行到這條指令時,解析動作才能進行。
解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符這七類
分別對應CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info,這前四種基本都是在解析時便可以替換為直接引用
CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic_info
這四種於動態語言聯絡緊密,為此我們需要明白解析與分派的區別
先從前四種開始說起
我們前面的8個符號引用,分別有自己單獨的常量表,其中記錄了去往哪查詢自己的程式碼的索引值,去呼叫欄位表和方法表中對於欄位和方法的定義
編譯器通過方法區預存的常量表解讀了class檔案中的位元組碼中的各個常量,建立了常量池,但是常量池中的儲存仍是依據字面量的索引,由字面量項儲存了一些字面量實現的資訊,並沒有真正的記憶體保留他,而我們的欄位表,方法表等依據name_index引用常量池中的常量項,但他們只儲存宣告的部分,至於初始化和方法體的實現,常常是放置在code中,code一般會在欄位表或方法表的後面
而我們的解析的作用就是將如CONSTANT_Class_info的字元引用找到字面量記錄的全限定類名交由classLoader載入(載入階段已掃描過一遍欄位表、方法表等並已建立出了Class物件在堆中存放了靜態的資料介面)
將欄位表中的對於CONSTANT_Fieldref_info儲存的索引的字面量的讀取出的簡單名稱和欄位描述符去匹配class_index中由類載入器載入出來的類是否有相應欄位,有則返回直接引用
方法解析與介面方法解析也是與欄位大致一樣的查詢邏輯,但是都只是找到了方法的入口,並非實現了其中的程式碼 ,這時候我們可以思考一下native的直接引用的地址是哪裡呢,個人認為此時已經是相應的javah實現的.h檔案的實現cpp了(還不知道如何除錯檢視)
而到了方法呼叫階段,則需要依據方法型別來判斷方法是在編譯期可知,執行期不可變還是依據分派配合動態語言進行解析
方法的呼叫並不如同欄位、方法等的入口等將字元引用換成直接引用儲存一個入口就可,而是依據code中的位元組碼轉換成相應的指令命令,使得引用時可以直接呼叫指令進行方法的執行,其中jvm若是解釋執行,則是依據操作棧來進行位元組碼指令的運作的通過呼叫作業系統對CPU操作的API來實現功能,若是基於編譯後實現的暫存器的,則是直接交由暫存器硬體實現的指令集執行(如x86).
而如何執行code中的指令,就需要方法型別的區分,其也是依據位元組碼指令來的:
- invokestatic 用於呼叫靜態方法
- invokespecial 用於呼叫例項構造器
()方法、私有方法、父類中的方法 - invokevirtual 用於呼叫所有的虛方法
- invokeinterface 用於呼叫介面方法,會在執行時再確定一個實現該介面的物件
- invokedynamic 現在執行時動態解析出呼叫點限定符所引用的方法,然後在執行該方法
其中invokestatic喝invokespecial是在符合執行時指令集是固定的(包括1和2的四種和final,final是用invokevirtual實現,但是因為不可修改),因此可以直接將其依據相應表解析成指令集後放入堆中Class例項的記憶體中(class物件時讀取方法表等),並返回地址將字元引用改為直接引用,這種方法稱為非虛方法(當我們將常量池的字元引用解析到屬性表集合時
而其他方法稱為虛方法(如:Code),不像前面的靜態型別直接就去檢視Class例項是否有匹配返回地址,而是需要依據上面的五個指標型別進行是否直接查詢直接引用還是其他的實現再返回地址作為直接引用)
而虛方法需要依靠分派呼叫(過載與重寫)
- 靜態分派(過載)
- 動態分派(重寫)
- 單分派與多分派
為了提高動態分派效率,我們還專門在方法區中建立了虛方法表
-
最後便是初始化,用
, 收斂初始化類變數等,<其中client 會經常呼叫 ,準備階段的初始化是系統變數的預設值,這裡是我們自定義的>將執行權重轉移到程式本身的code實現上
我們已經知道我們在載入階段就在堆中實現了Class,使得我們能後續能為常量池中的常量項進行解析,最後會將解析後的常量池放到執行時常量池中進行呼叫
通過
若是基於棧的解釋執行,我們會依據各個方法建立棧幀,並用棧幀中的運算元棧實現位元組碼指令對作業系統對於CPUapi的呼叫執行code中的位元組碼指令,而位元組碼指令基本上都是零地址指令(他會對指令的讀取和數值的取出讀入等由幾個固定棧結構進行操作)。若是經過編譯的,則是依據編譯器,則依據暫存器的硬體實現的指令集進行解讀。兩者的不同主要在執行時前者需要將運算元出棧計算再入棧儲存,而後者則可以在cpu計算後直接儲存回暫存器運算元地址的位置上。
無論是c還是java,都是最後都是經過CPU對記憶體中某個記憶體地址那一部分的儲存值依據指令集進行修改,jni也不過是起到使得c方法編譯後的指令集的地址查詢能符合java地址直接引用的規則,而其會將入口地址放入lib中使得能通過c中的表查詢到入口(c入口地址都通過連結寫到了lib中,而java的虛方法還接收者需要執行時根據實際型別選擇版本等),因此無論是JNI中java對於C物件的呼叫還是c對於java物件的呼叫,只要有相應的地址,原始碼編譯成的相應的指令集都可以實現對不同語言物件的操作,作業系統也無外乎用自己實現的指令集組合用cpu修改其他各個硬體的電平狀態來達到控制所有硬體各種語言的目的。
而直譯器和編譯器通過運算元棧或者暫存器都呼叫系統API的實現,都是基於執行引擎呼叫該些後端編譯器進行的,
執行引擎是我們與作業系統互動的最直接的部分,我們最後將class類加入方法區後並不是就可以直接加入對JVM的其他結構,而是需要執行引擎使用後端編譯器進行解釋編譯時,javac輸出的位元組碼指令流,基本上是一種基於棧的指令集結構,是直譯器和即時編譯器執行優化的方式,是基本將中間碼在JVM棧上執行的,由棧儲存值的,
而提前編譯編譯後的或者即時編譯後的直接的二進位制檔案,則是多基於暫存器直接實現(如x86的二地址指令集),但若是原始碼啟動,需要你的程式剛開始需要較長的時間去編譯,若是二進位制版本的,則需要為每一個指令集專門編譯一個版本而且也不一定完全適配,效率也沒有原始碼編譯的更快(但其實相差無幾)
我們這時候也不難想象ACC_NATIVE是如何通過本地方法棧找到對c方法地址的直接引用放入執行時常量池中,呼叫方法時java棧通過運算元棧找到虛擬機器c的方法指令的位置(而其中多是對作業系統API的呼叫),將方法中的指令經由CPU(使用者執行緒)計算結果傳給作業系統API(也是地址,再呼叫作業系統實現的指令,至於是直接組合語言編譯結果還是高階語言的編譯結果就不得而知了),作業系統將自身編譯的指令送入CPU計算,返回我們想要的結果的了,到了這一步我終於明白為什麼知道面試官為什麼喜歡懂得作業系統核心的了,因為作業系統中實現了很多如網路,shell顯示,IO的,其中的API就是相應實現後編譯的指令集的入口,而且要考慮很多的優化和併發,其中特別是要自己實現使用者執行緒去呼叫CPU還是要自己的使用者執行緒呼叫作業系統的API經過作業系統的核心執行緒使用CPU,執行緒呼叫CPU後得到的運算結果,要自己去呼叫IO等還是回作業系統的API實現都是很複雜的需要考慮編譯器能否實現準確的編譯後能否適配的,還需要藉助組合語言來檢視除錯優化,太難了
本地方法棧和作業系統的關係可以參考:https://blog.csdn.net/yfqnihao/article/details/8289363