native到CPU
- 2020 年 9 月 19 日
- 筆記
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)
編寫範例://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實現的ClassLoader//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中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實現都是很複雜的需要考慮編譯器能否實現準確的編譯後能否適配的,還需要藉助彙編語言來查看調試優化,太難了
本地方法棧和作業系統的關係可以參考://blog.csdn.net/yfqnihao/article/details/8289363