1. 程式人生 > 實用技巧 >[轉]JAVA虛擬機器體系結構

[轉]JAVA虛擬機器體系結構

JAVA虛擬機器的生命週期

  一個執行時的Java虛擬機器例項的天職是:負責執行一個java程式。當啟動一個Java程式時,一個虛擬機器例項也就誕生了。當該程式關閉退出,這個虛擬機器例項也就隨之消亡。如果同一臺計算機上同時執行三個Java程式,將得到三個Java虛擬機器例項。每個Java程式都運行於它自己的Java虛擬機器例項中。

  Java虛擬機器例項通過呼叫某個初始類的main()方法來執行一個Java程式。而這個main()方法必須是共有的(public)、靜態的(static)、返回值為void,並且接受一個字串陣列作為引數。任何擁有這樣一個main()方法的類都可以作為Java程式執行的起點。

public class Test {    public static void main(String[] args) {        // TODO Auto-generated method stub        System.out.println("Hello World");    }}

  在上面的例子中,Java程式初始類中的main()方法,將作為該程式初始執行緒的起點,任何其他的執行緒都是由這個初始執行緒啟動的。

  在Java虛擬機器內部有兩種執行緒:守護執行緒和非守護執行緒。守護執行緒通常是由虛擬機器自己使用的,比如執行垃圾收集任務的執行緒。但是,Java程式也可以把它建立的任何執行緒標記為守護執行緒。而Java程式中的初始執行緒——就是開始於main()的那個,是非守護執行緒。

  只要還有任何非守護執行緒在執行,那麼這個Java程式也在繼續執行。當該程式中所有的非守護執行緒都終止時,虛擬機器例項將自動退出。假若安全管理器允許,程式本身也能夠通過呼叫Runtime類或者System類的exit()方法來退出。

JAVA虛擬機器的體系結構

  下圖是JAVA虛擬機器的結構圖,每個Java虛擬機器都有一個類裝載子系統,它根據給定的全限定名來裝入型別(類或介面)。同樣,每個Java虛擬機器都有一個執行引擎,它負責執行那些包含在被裝載類的方法中的指令。

  

  當JAVA虛擬機器執行一個程式時,它需要記憶體來儲存許多東西,例如:位元組碼、從已裝載的class檔案中得到的其他資訊、程式建立的物件、傳遞給方法的引數,返回值、區域性變數等等。Java虛擬機器把這些東西都組織到幾個“執行時資料區”中,以便於管理。

  某些執行時資料區是由程式中所有執行緒共享的,還有一些則只能由一個執行緒擁有。每個Java虛擬機器例項都有一個方法區以及一個堆,它們是由該虛擬機器例項中所有的執行緒共享的。當虛擬機器裝載一個class檔案時,它會從這個class檔案包含的二進位制資料中解析型別資訊。然後把這些型別資訊放到方法區中。當程式執行時,虛擬機器會把所有該程式在執行時建立的物件都放到堆中。

  

  當每一個新執行緒被建立時,它都將得到它自己的PC暫存器(程式計數器)以及一個Java棧,如果執行緒正在執行的是一個Java方法(非本地方法),那麼PC暫存器的值將總是指向下一條將被執行的指令,而它的Java棧則總是儲存該執行緒中Java方法呼叫的狀態——包括它的區域性變數,被呼叫時傳進來的引數、返回值,以及運算的中間結果等等。而本地方法呼叫的狀態,則是以某種依賴於具體實現的方法儲存在本地方法棧中,也可能是在暫存器或者其他某些與特定實現相關的記憶體區中。

  Java棧是由許多棧幀(stack frame)組成的,一個棧幀包含一個Java方法呼叫的狀態。當執行緒呼叫一個Java方法時,虛擬機器壓入一個新的棧幀到該執行緒的Java棧中,當該方法返回時,這個棧幀被從Java棧中彈出並拋棄。

  Java虛擬機器沒有暫存器,其指令集使用Java棧來儲存中間資料。這樣設計的原因是為了保持Java虛擬機器的指令集儘量緊湊、同時也便於Java虛擬機器在那些只有很少通用暫存器的平臺上實現。另外,Java虛擬機器這種基於棧的體系結構,也有助於執行時某些虛擬機器實現的動態編譯器和即時編譯器的程式碼優化。

  下圖描繪了Java虛擬機器為每一個執行緒建立的記憶體區,這些記憶體區域是私有的,任何執行緒都不能訪問另一個執行緒的PC暫存器或者Java棧。

  

  上圖展示了一個虛擬機器例項的快照,它有三個執行緒正在執行。執行緒1和執行緒2都正在執行Java方法,而執行緒3則正在執行一個本地方法。

  Java棧都是向下生長的,而棧頂都顯示在圖的底部。當前正在執行的方法的棧幀則以淺色表示,對於一個正在執行Java方法的執行緒而言,它的PC暫存器總是指向下一條將被執行的指令。比如執行緒1和執行緒2都是以淺色顯示的,由於執行緒3當前正在執行一個本地方法,因此,它的PC暫存器——以深色顯示的那個,其值是不確定的。

 資料型別

  Java虛擬機器是通過某些資料型別來執行計算的,資料型別可以分為兩種:基本型別和引用型別,基本型別的變數持有原始值,而引用型別的變數持有引用值。

  

  Java語言中的所有基本型別同樣也都是Java虛擬機器中的基本型別。但是boolean有點特別,雖然Java虛擬機器也把boolean看做基本型別,但是指令集對boolean只有很有限的支援,當編譯器把Java原始碼編譯為位元組碼時,它會用int或者byte來表示boolean。在Java虛擬機器中,false是由整數零來表示的,所有非零整數都表示true,涉及boolean值的操作則會使用int。另外,boolean陣列是當做byte陣列來訪問的,但是在“堆”區,它也可以被表示為位域。

  Java虛擬機器還有一個只在內部使用的基本型別:returnAddress,Java程式設計師不能使用這個型別,這個基本型別被用來實現Java程式中的finally子句。該型別是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作碼的指標。returnAddress型別不是簡單意義上的數值,不屬於任何一種基本型別,並且它的值是不能被執行中的程式所修改的。

  Java虛擬機器的引用型別被統稱為“引用(reference)”,有三種引用型別:類型別、介面型別、以及陣列型別,它們的值都是對動態建立物件的引用。類型別的值是對類例項的引用;陣列型別的值是對陣列物件的引用,在Java虛擬機器中,陣列是個真正的物件;而介面型別的值,則是對實現了該介面的某個類例項的引用。還有一種特殊的引用值是null,它表示該引用變數沒有引用任何物件。

  JAVA中方法引數的引用傳遞

  java中引數的傳遞有兩種,分別是按值傳遞和按引用傳遞。按值傳遞不必多說,下面就說一下按引用傳遞。

  “當一個物件被當作引數傳遞到一個方法”,這就是所謂的按引用傳遞。

public class User {        private String name;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    }
public class Test {        public void set(User user){        user.setName("hello world");    }        public static void main(String[] args) {                Test test = new Test();        User user = new User();        test.set(user);        System.out.println(user.getName());    }}

  上面程式碼的輸出結果是“hello world”,這不必多說,那如果將set方法改為如下,結果會是多少呢?

    public void set(User user){        user.setName("hello world");        user = new User();        user.setName("change");    }

  答案依然是“hello world”,下面就讓我們來分析一下如上程式碼。

  首先

User user = new User();

  是在堆中建立了一個物件,並在棧中建立了一個引用,此引用指向該物件,如下圖:

test.set(user);

  是將引用user作為引數傳遞到set方法,注意:這裡傳遞的並不是引用本身,而是一個引用的拷貝。也就是說這時有兩個引用(引用和引用的拷貝)同時指向堆中的物件,如下圖:

user.setName("hello world");

  在set()方法中,“user引用的拷貝”操作堆中的User物件,給name屬性設定字串"hello world"。如下圖:

  

user = new User();

  在set()方法中,又建立了一個User物件,並將“user引用的拷貝”指向這個在堆中新建立的物件,如下圖:

  

user.setName("change");

  在set()方法中,“user引用的拷貝”操作的是堆中新建立的User物件。

  set()方法執行完畢,目光再回到mian()方法

System.out.println(user.getName());

  因為之前,"user引用的拷貝"已經將堆中的User物件的name屬性設定為了"hello world",所以當main()方法中的user呼叫getName()時,列印的結果就是"hello world"。如下圖:

  

 類裝載子系統

  在JAVA虛擬機器中,負責查詢並裝載型別的那部分被稱為類裝載子系統。

  JAVA虛擬機器有兩種類裝載器:啟動類裝載器和使用者自定義類裝載器。前者是JAVA虛擬機器實現的一部分,後者則是Java程式的一部分。由不同的類裝載器裝載的類將被放在虛擬機器內部的不同名稱空間中。

  類裝載器子系統涉及Java虛擬機器的其他幾個組成部分,以及幾個來自java.lang庫的類。比如,使用者自定義的類裝載器是普通的Java物件,它的類必須派生自java.lang.ClassLoader類。ClassLoader中定義的方法為程式提供了訪問類裝載器機制的介面。此外,對於每一個被裝載的型別,JAVA虛擬機器都會為它建立一個java.lang.Class類的例項來代表該型別。和所有其他物件一樣,使用者自定義的類裝載器以及Class類的例項都放在記憶體中的堆區,而裝載的型別資訊則都位於方法區。

  類裝載器子系統除了要定位和匯入二進位制class檔案外,還必須負責驗證被匯入類的正確性,為類變數分配並初始化記憶體,以及幫助解析符號引用。這些動作必須嚴格按以下順序進行:

  (1)裝載——查詢並裝載型別的二進位制資料。

  (2)連線——指向驗證、準備、以及解析(可選)。

    ● 驗證  確保被匯入型別的正確性。

    ● 準備  為類變數分配記憶體,並將其初始化為預設值。

    ● 解析  把型別中的符號引用轉換為直接引用。

  (3)初始化——把類變數初始化為正確初始值。

  每個JAVA虛擬機器實現都必須有一個啟動類裝載器,它知道怎麼裝載受信任的類。

  每個類裝載器都有自己的名稱空間,其中維護著由它裝載的型別。所以一個Java程式可以多次裝載具有同一個全限定名的多個型別。這樣一個型別的全限定名就不足以確定在一個Java虛擬機器中的唯一性。因此,當多個類裝載器都裝載了同名的型別時,為了惟一地標識該型別,還要在型別名稱前加上裝載該型別(指出它所位於的名稱空間)的類裝載器標識。

 方法區

  在Java虛擬機器中,關於被裝載型別的資訊儲存在一個邏輯上被稱為方法區的記憶體中。當虛擬機器裝載某個型別時,它使用類裝載器定位相應的class檔案,然後讀入這個class檔案——1個線性二進位制資料流,然後它傳輸到虛擬機器中,緊接著虛擬機器提取其中的型別資訊,並將這些資訊儲存到方法區。該型別中的類(靜態)變數同樣也是儲存在方法區中。

  JAVA虛擬機器在內部如何儲存型別資訊,這是由具體實現的設計者來決定的。

  當虛擬機器執行Java程式時,它會查詢使用儲存在方法區中的型別資訊。由於所有執行緒都共享方法區,因此它們對方法區資料的訪問必須被設計為是執行緒安全的。比如,假設同時有兩個執行緒都企圖訪問一個名為Lava的類,而這個類還沒有被裝入虛擬機器,那麼,這時只應該有一個執行緒去裝載它,而另一個執行緒則只能等待。

  對於每個裝載的型別,虛擬機器都會在方法區中儲存以下型別資訊:

  ● 這個型別的全限定名

  ● 這個型別的直接超類的全限定名(除非這個型別是java.lang.Object,它沒有超類)

  ● 這個型別是類型別還是介面型別

  ● 這個型別的訪問修飾符(public、abstract或final的某個子集)

  ● 任何直接超介面的全限定名的有序列表

  除了上面列出的基本型別資訊外,虛擬機器還得為每個被裝載的型別儲存以下資訊:

  ● 該型別的常量池

  ● 欄位資訊

  ● 方法資訊

  ● 除了常量以外的所有類(靜態)變數

  ● 一個到類ClassLoader的引用

  ● 一個到Class類的引用

  常量池

  虛擬機器必須為每個被裝載的型別維護一個常量池。常量池就是該型別所用常量的一個有序集合,包括直接常量和對其他型別、欄位和方法的符號引用。池中的資料項就像陣列一樣是通過索引訪問的。因為常量池儲存了相應型別所用到的所有型別、欄位和方法的符號引用,所以它在Java程式的動態連線中起著核心的作用。

  欄位資訊

  對於型別中宣告的每一個欄位。方法區中必須儲存下面的資訊。除此之外,這些欄位在類或者介面中的宣告順序也必須儲存。

  ○ 欄位名

  ○ 欄位的型別

  ○ 欄位的修飾符(public、private、protected、static、final、volatile、transient的某個子集)

  方法資訊

  對於型別中宣告的每一個方法,方法區中必須儲存下面的資訊。和欄位一樣,這些方法在類或者介面中的宣告順序也必須儲存。

  ○ 方法名

  ○ 方法的返回型別(或void)

  ○ 方法引數的數量和型別(按宣告順序)

  ○ 方法的修飾符(public、private、protected、static、final、synchronized、native、abstract的某個子集)

  除了上面清單中列出的條目之外,如果某個方法不是抽象的和本地的,它還必須儲存下列資訊:

  ○ 方法的位元組碼(bytecodes)

  ○ 運算元棧和該方法的棧幀中的區域性變數區的大小

  ○ 異常表

  類(靜態)變數

  類變數是由所有類例項共享的,但是即使沒有任何類例項,它也可以被訪問。這些變數只與類有關——而非類的例項,因此它們總是作為型別資訊的一部分而儲存在方法區。除了在類中宣告的編譯時常量外,虛擬機器在使用某個類之前,必須在方法區中為這些類變數分配空間。

  而編譯時常量(就是那些用final宣告以及用編譯時已知的值初始化的類變數)則和一般的類變數處理方式不同,每個使用編譯時常量的型別都會複製它的所有常量到自己的常量池中,或嵌入到它的位元組碼流中。作為常量池或位元組碼流的一部分,編譯時常量儲存在方法區中——就和一般的類變數一樣。但是當一般的類變數作為宣告它們的型別的一部分資料面儲存的時候,編譯時常量作為使用它們的型別的一部分而儲存。

  指向ClassLoader類的引用

  每個型別被裝載的時候,虛擬機器必須跟蹤它是由啟動類裝載器還是由使用者自定義類裝載器裝載的。如果是使用者自定義類裝載器裝載的,那麼虛擬機器必須在型別資訊中儲存對該裝載器的引用。這是作為方法表中的型別資料的一部分儲存的。

  虛擬機器會在動態連線期間使用這個資訊。當某個型別引用另一個型別的時候,虛擬機器會請求裝載發起引用型別的類裝載器來裝載被引用的型別。這個動態連線的過程,對於虛擬機器分離名稱空間的方式也是至關重要的。為了能夠正確地執行動態連線以及維護多個名稱空間,虛擬機器需要在方法表中得知每個類都是由哪個類裝載器裝載的。

  指向Class類的引用

  對於每一個被裝載的型別(不管是類還是介面),虛擬機器都會相應地為它建立一個java.lang.Class類的例項,而且虛擬機器還必須以某種方式把這個例項和儲存在方法區中的型別資料關聯起來。

  在Java程式中,你可以得到並使用指向Class物件的引用。Class類中的一個靜態方法可以讓使用者得到任何已裝載的類的Class例項的引用。

public static Class<?> forName(String className)

  比如,如果呼叫forName("java.lang.Object"),那麼將得到一個代表java.lang.Object的Class物件的引用。可以使用forName()來得到代表任何包中任何型別的Class物件的引用,只要這個型別可以被(或者已經被)裝載到當前名稱空間中。如果虛擬機器無法把請求的型別裝載到當前名稱空間,那麼會丟擲ClassNotFoundException異常。

  另一個得到Class物件引用的方法是,可以呼叫任何物件引用的getClass()方法。這個方法被來自Object類本身的所有物件繼承:

public final native Class<?> getClass();

  比如,如果你有一個到java.lang.Integer類的物件的引用,那麼你只需簡單地呼叫Integer物件引用的getClass()方法,就可以得到表示java.lang.Integer類的Class物件。

  方法區使用例項

  為了展示虛擬機器如何使用方法區中的資訊,下面來舉例說明:

class Lava {    private int speed = 5;    void flow(){            }}
public class Volcano {        public static void main(String[] args){        Lava lava = new Lava();        lava.flow();    }}

  不同的虛擬機器實現可能會用完全不同的方法來操作,下面描述的只是其中一種可能——但並不是僅有的一種。

  要執行Volcano程式,首先得以某種“依賴於實現的”方式告訴虛擬機器“Volcano”這個名字。之後,虛擬機器將找到並讀入相應的class檔案“Volcano.class”,然後它會從匯入的class檔案裡的二進位制資料中提取型別資訊並放到方法區中。通過執行儲存在方法區中的位元組碼,虛擬機器開始執行main()方法,在執行時,它會一直持有指向當前類(Volcano類)的常量池(方法區中的一個數據結構)的指標。

  注意:虛擬機器開始執行Volcano類中main()方法的位元組碼的時候,儘管Lava類還沒被裝載,但是和大多數(也許所有)虛擬機器實現一樣,它不會等到把程式中用到的所有類都裝載後才開始執行。恰好相反,它只會需要時才裝載相應的類。

  main()的第一條指令告知虛擬機器為列在常量池第一項的類分配足夠的記憶體。所以虛擬機器使用指向Volcano常量池的指標找到第一項,發現它是一個對Lava類的符號引用,然後它就檢查方法區,看Lava類是否已經被載入了。

  這個符號引用僅僅是一個給出了類Lava的全限定名“Lava”的字串。為了能讓虛擬機器儘可能快地從一個名稱找到類,虛擬機器的設計者應當選擇最佳的資料結構和演算法。

  當虛擬機發現還沒有裝載過名為“Lava”的類時,它就開始查詢並裝載檔案“Lava.class”,並把從讀入的二進位制資料中提取的型別資訊放在方法區中。

  緊接著,虛擬機器以一個直接指向方法區Lava類資料的指標來替換常量池第一項(就是那個字串“Lava”),以後就可以用這個指標來快速地訪問Lava類了。這個替換過程稱為常量池解析,即把常量池中的符號引用替換為直接引用。

  終於,虛擬機器準備為一個新的Lava物件分配記憶體。此時它又需要方法區中的資訊。還記得剛剛放到Volcano類常量池第一項的指標嗎?現在虛擬機器用它來訪問Lava型別資訊,找出其中記錄的這樣一條資訊:一個Lava物件需要分配多少堆空間。

  JAVA虛擬機器總能夠通過儲存與方法區的型別資訊來確定一個物件需要多少記憶體,當JAVA虛擬機器確定了一個Lava物件的大小後,它就在堆上分配這麼大的空間,並把這個物件例項的變數speed初始化為預設初始值0。

  當把新生成的Lava物件的引用壓到棧中,main()方法的第一條指令也完成了。接下來的指令通過這個引用呼叫Java程式碼(該程式碼把speed變數初始化為正確初始值5)。另一條指令將用這個引用呼叫Lava物件引用的flow()方法。

 堆

  Java程式在執行時建立的所有類例項或陣列都放在同一個堆中。而一個JAVA虛擬機器例項中只存在一個堆空間,因此所有執行緒都將共享這個堆。又由於一個Java程式獨佔一個JAVA虛擬機器例項,因而每個Java程式都有它自己的堆空間——它們不會彼此干擾。但是同一個Java程式的多個執行緒卻共享著同一個堆空間,在這種情況下,就得考慮多執行緒訪問物件(堆資料)的同步問題了。

  JAVA虛擬機器有一條在堆中分配新物件的指令,卻沒有釋放記憶體的指令,正如你無法用Java程式碼區明確釋放一個物件一樣。虛擬機器自己負責決定如何以及何時釋放不再被執行的程式引用的物件所佔據的記憶體。通常,虛擬機器把這個任務交給垃圾收集器。

  陣列的內部表示

  在Java中,陣列是真正的物件。和其他物件一樣,陣列總是儲存在堆中。同樣,陣列也擁有一個與它們的類相關聯的Class例項,所有具有相同維度和型別的陣列都是同一個類的例項,而不管陣列的長度(多維陣列每一維的長度)是多少。例如一個包含3個int整數的陣列和一個包含300個整數的陣列擁有同一個類。陣列的長度只與例項資料有關。

  陣列類的名稱由兩部分組成:每一維用一個方括號“[”表示,用字元或字串表示元素型別。比如,元素型別為int整數的、一維陣列的類名為“[I”,元素型別為byte的三維陣列為“[[[B”,元素型別為Object的二維陣列為“[[Ljava/lang/Object”。

  多維陣列被表示為陣列的陣列。比如,int型別的二維陣列,將表示為一個一維陣列,其中的每一個元素是一個一維int陣列的引用,如下圖:

  

  在堆中的每個陣列物件還必須儲存的資料時陣列的長度、陣列資料,以及某些指向陣列的類資料的引用。虛擬機器必須能夠通過一個數組物件的引用得到此陣列的長度,通過索引訪問其元素(期間要檢查陣列邊界是否越界),呼叫所有陣列的直接超類Object宣告的方法等等。

 程式計數器

  對於一個執行中的Java程式而言,其中的每一個執行緒都有它自己的PC(程式計數器)暫存器,它是在該執行緒啟動時建立的,PC暫存器的大小是一個字長,因此它既能夠持有一個本地指標,也能夠持有一個returnAddress。當執行緒執行某個Java方法時,PC暫存器的內容總是下一條將被執行指令的“地址”,這裡的“地址”可以是一個本地指標,也可以是在方法位元組碼中相對於該方法起始指令的偏移量。如果該執行緒正在執行一個本地方法,那麼此時PC暫存器的值是“undefined”。

 Java棧

  每當啟動一個新執行緒時,Java虛擬機器都會為它分配一個Java棧。Java棧以幀為單位儲存執行緒的執行狀態。虛擬機器只會直接對Java棧執行兩種操作:以幀為單位的壓棧和出棧。

  某個執行緒正在執行的方法被稱為該執行緒的當前方法,當前方法使用的棧幀稱為當前幀,當前方法所屬的類稱為當前類,當前類的常量池稱為當前常量池。線上程執行一個方法時,它會跟蹤當前類和當前常量池。此外,當虛擬機器遇到棧內操作指令時,它對當前幀內資料執行操作。

  每當執行緒呼叫一個Java方法時,虛擬機器都會在該執行緒的Java棧中壓入一個新幀。而這個新幀自然就成為了當前幀。在執行這個方法時,它使用這個幀來儲存引數、區域性變數、中間運算結果等資料。

  Java方法可以以兩種方式完成。一種通過return返回的,稱為正常返回;一種是通過丟擲異常而異常終止的。不管以哪種方式返回,虛擬機器都會將當前幀彈出Java棧然後釋放掉,這樣上一個方法的幀就成為當前幀了。

  Java幀上的所有資料都是此執行緒私有的。任何執行緒都不能訪問另一個執行緒的棧資料,因此我們不需要考慮多執行緒情況下棧資料的訪問同步問題。當一個執行緒呼叫一個方法時,方法的的區域性變數儲存在呼叫執行緒Java棧的幀中。只有一個執行緒能總是訪問那些區域性變數,即呼叫方法的執行緒。

 本地方法棧

  前面提到的所有執行時資料區都是Java虛擬機器規範中明確定義的,除此之外,對於一個執行中的Java程式而言,它還可能會用到一些跟本地方法相關的資料區。當某個執行緒呼叫一個本地方法時,它就進入了一個全新的並且不再受虛擬機器限制的世界。本地方法可以通過本地方法介面來訪問虛擬機器的執行時資料區,但不止如此,它還可以做任何它想做的事情。

  本地方法本質上時依賴於實現的,虛擬機器實現的設計者們可以自由地決定使用怎樣的機制來讓Java程式呼叫本地方法。

  任何本地方法介面都會使用某種本地方法棧。當執行緒呼叫Java方法時,虛擬機器會建立一個新的棧幀並壓入Java棧。然而當它呼叫的是本地方法時,虛擬機器會保持Java棧不變,不再線上程的Java棧中壓入新的幀,虛擬機器只是簡單地動態連線並直接呼叫指定的本地方法。

  如果某個虛擬機器實現的本地方法介面是使用C連線模型的話,那麼它的本地方法棧就是C棧。當C程式呼叫一個C函式時,其棧操作都是確定的。傳遞給該函式的引數以某個確定的順序壓入棧,它的返回值也以確定的方式傳回呼叫者。同樣,這就是虛擬機器實現中本地方法棧的行為。

  很可能本地方法介面需要回調Java虛擬機器中的Java方法,在這種情況下,該執行緒會儲存本地方法棧的狀態並進入到另一個Java棧。

  下圖描繪了這樣一個情景,就是當一個執行緒呼叫一個本地方法時,本地方法又回撥虛擬機器中的另一個Java方法。這幅圖展示了JAVA虛擬機器內部執行緒執行的全景圖。一個執行緒可能在整個生命週期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉。  

  該執行緒首先呼叫了兩個Java方法,而第二個Java方法又呼叫了一個本地方法,這樣導致虛擬機器使用了一個本地方法棧。假設這是一個C語言棧,其間有兩個C函式,第一個C函式被第二個Java方法當做本地方法呼叫,而這個C函式又呼叫了第二個C函式。之後第二個C函式又通過本地方法介面回調了一個Java方法(第三個Java方法),最終這個Java方法又呼叫了一個Java方法(它成為圖中的當前方法)。