.h頭文件和.cpp是編譯時必須的,lib是連接時須要的,dll是運行時須要的。前端
.h:聲明函數接口java
.cpp:c++語言實現的功能源碼linux
.lib :c++
LIB有兩種,一種是靜態庫,好比C-Runtime庫,這種LIB中有函數的實現代碼,通常用在靜態連編上,它是將LIB中的代碼加入目標模塊(EXE或者DLL)文件中,因此連接好了以後,LIB文件就沒有用了。面試
一種LIB是和DLL配合使用的,裏面沒有代碼,代碼在DLL中,這種LIB是用在靜態調用DLL上的,因此起的做用也是連接做用,連接完成了,LIB也沒用了。至於動態調用DLL的話,根本用不上LIB文件。 目標模塊(EXE或者DLL)文件生成以後,就用不着LIB文件了。shell
.dll:windows
動態連接庫英文爲DLL,是Dynamic Link Library的縮寫。DLL是一個包含可由多個程序,同時使用的代碼和數據的庫。後端
當程序使用 DLL 時,具備如下的優勢: 使用較少的資源,當多個程序使用同一個函數庫時,DLL 能夠減小在磁盤和物理內存中加載的代碼的重複量(運行時須要的庫是須要加入內存的)。api
.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 + .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是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中的哪一個方法了
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);
咱們編譯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實例是否有匹配返回地址,而是須要依據上面的五個指針類型進行是否直接查找直接引用仍是其餘的實現再返回地址做爲直接引用)
而虛方法須要依靠分派調用(重載與重寫)
- 靜態分派(重載)
- 動態分派(重寫)
- 單分派與多分派
爲了提升動態分派效率,咱們還專門在方法區中創建了虛方法表
最後即是初始化,用
咱們已經知道咱們在加載階段就在堆中實現了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