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接口文件(頭文件):

  1. 編寫帶有native聲明的方法的java類

  2. 使用javac命令編譯所編寫的java類

  3. 然後使用javah + java類名生成擴展名為h的頭文件

  4. 使用C/C++實現本地方法

  5. 將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裝入方法區開始,就是類的加載過程了

具體流程是

  1. 加載(既可以由JVM本身加載入方法區,也可自定義的classLoder選取需要加載的class,通過JNI調用)

    通過一個類的全限定類名來獲取定義此類的二進制位元組流

    將這個位元組流所代表的靜態結構轉化為方法區的運行時數據結構

    在內存(堆)中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口(單例模式)

    至於什麼時候加載,除了遇到new、getstatic、putstatic、invokestatic四條指令時,必須立刻加載··到初始化完成

  2. 驗證(java源碼本身的編譯是相對安全的,但是位元組碼的內容可能會加入惡意代碼,因此需要驗證)

    文件格式驗證(位元組流的各個部分劃分是否符合規範)

    元數據驗證(對元數據信息中的數據類型檢驗)

    位元組碼校驗(對方法體中的內容進行校驗,較為複雜耗時,jdk6後可以將權重部分移向了javac)

    符號引用校驗(在解析階段同時進行)

  3. 準備

    正式為類中定義的變量(即靜態變量,被static修飾的變量)分配內存並設置類變量初始值的階段。從概念上講,這些變量所使用的內存都應當在方法區中進行分配,但需要注意的是方法區本身是一個邏輯層面的概念,其實現在不同的版本,不同的虛擬機上可能分佈在不同的內存空間,如同JMM之於JVM一般

    jdk 8之前,HotSpot團隊選擇把收集器的分代擴展至方法區,由垃圾收集器統一收集,省去專門寫一個獨立的管理方法區的方法,而方法區的存儲內容與前面的分代的更新換代條件大不相同,所以專門劃分了個永久代,但這容易導致更多的內存溢出問題

    jdk6hotspot就將捨棄永久代放進了發展策略,逐步改用成了用直接內存(Direct Memory)中的元空間等來存儲方法區的內容,實現單獨的回收管理,

    jdk7已經將字符串常量池、靜態變量等移出,jdk8以全部移出

    jdk8 時類變量會隨着Class對象一起存放到Java堆中,類型信息則放到了直接內存中了。

    圖網上找的(其中類信息也稱為靜態常量池)

    java內存結構

  4. 解析

    解析階段是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中的指令,就需要方法類型的區分,其也是依據位元組碼指令來的:

    1. invokestatic 用於調用靜態方法
    2. invokespecial 用於調用實例構造器()方法、私有方法、父類中的方法
    3. invokevirtual 用於調用所有的虛方法
    4. invokeinterface 用於調用接口方法,會在運行時再確定一個實現該接口的對象
    5. invokedynamic 現在運行時動態解析出調用點限定符所引用的方法,然後在執行該方法

    其中invokestatic喝invokespecial是在符合運行時指令集是固定的(包括1和2的四種和final,final是用invokevirtual實現,但是因為不可修改),因此可以直接將其依據相應表解析成指令集後放入堆中Class實例的內存中(class對象時讀取方法表等),並返回地址將字符引用改為直接引用,這種方法稱為非虛方法(當我們將常量池的字符引用解析到屬性表集合時

    而其他方法稱為虛方法(如:Code),不像前面的靜態類型直接就去查看Class實例是否有匹配返回地址,而是需要依據上面的五個指針類型進行是否直接查找直接引用還是其他的實現再返回地址作為直接引用)

    而虛方法需要依靠分派調用(重載與重寫)

    1. 靜態分派(重載)
    2. 動態分派(重寫)
    3. 單分派與多分派

    為了提高動態分派效率,我們還專門在方法區中建立了虛方法表

  5. 最後便是初始化,用,收斂初始化類變量等,<其中client 會經常調用,準備階段的初始化是系統變量的默認值,這裡是我們自定義的>將運行權重轉移到程序本身的code實現上

我們已經知道我們在加載階段就在堆中實現了Class,使得我們能後續能為常量池中的常量項進行解析,最後會將解析後的常量池放到運行時常量池中進行調用

通過 我們開始依據運行時常量池中的方法順序依據直接引用的地址調用code中的位元組碼指令(此時,無論是哪種指令集,指令集只是我們將二進制按位數劃分助記而已,已經都是0101這種cpu能解讀的模式了,但都需要按照(操作碼字段|地址碼字段)來傳送給CPU,不同的指令集只是將二進制串劃分成不同的段交給CPU,CPU所能讀取指令長度會依據指令的操作碼判斷是幾地址指令,比如add它可以有00,01,10,11 分別表示1234地址指令)

若是基於棧的解釋執行,我們會依據各個方法創建棧幀,並用棧幀中的操作數棧實現位元組碼指令對操作系統對於CPUapi的調用運行code中的位元組碼指令,而位元組碼指令基本上都是零地址指令(他會對指令的讀取和數值的取出讀入等由幾個固定棧結構進行操作)。若是經過編譯的,則是依據編譯器,則依據寄存器的硬件實現的指令集進行解讀。兩者的不同主要在運行時前者需要將操作數出棧計算再入棧保存,而後者則可以在cpu計算後直接保存回寄存器操作數地址的位置上。

無論是c還是java,都是最後都是經過CPU對內存中某個內存地址那一部分的存儲值依據指令集進行修改,因此無論是JNI中java對於C對象的調用還是c對於java對象的調用,只要有相應的地址,源碼編譯成的相應的指令集都可以實現對不同語言對象的操作,操作系統也無外乎用自己實現的指令集組合用cpu修改其他各個硬件的電平狀態來達到控制所有硬件各種語言的目的。

而解釋器和編譯器通過操作數棧或者寄存器都調用系統API的實現,都是基於執行引擎調用該些後端編譯器進行的,等javac自己加上去的方法會調用執行引擎依據自己的實現選擇使用上兩者。

執行引擎是我們與操作系統交互的最直接的部分,我們最後將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