Java虛擬機(jī)體系結(jié)構(gòu)
時(shí)間:2023-06-26 10:57:01 | 來(lái)源:網(wǎng)站運(yùn)營(yíng)
時(shí)間:2023-06-26 10:57:01 來(lái)源:網(wǎng)站運(yùn)營(yíng)
Java虛擬機(jī)體系結(jié)構(gòu):一個(gè)運(yùn)行時(shí)的Java虛擬機(jī)實(shí)例的天職是:負(fù)責(zé)運(yùn)行一個(gè)java程序。當(dāng)啟動(dòng)一個(gè)Java程序時(shí),一個(gè)虛擬機(jī)實(shí)例也就誕生了。當(dāng)該程序關(guān)閉退出,這個(gè)虛擬機(jī)實(shí)例也就隨之消亡。如果同一臺(tái)計(jì)算機(jī)上同時(shí)運(yùn)行三個(gè)Java程序,將得到三個(gè)Java虛擬機(jī)實(shí)例。每個(gè)Java程序都運(yùn)行于它自己的Java虛擬機(jī)實(shí)例中。
Java虛擬機(jī)實(shí)例通過(guò)調(diào)用某個(gè)初始類的main()方法來(lái)運(yùn)行一個(gè)Java程序。而這個(gè)main()方法必須是共有的(public)、靜態(tài)的(static)、返回值為void,并且接受一個(gè)字符串?dāng)?shù)組作為參數(shù)。任何擁有這樣一個(gè)main()方法的類都可以作為Java程序運(yùn)行的起點(diǎn)。
public class Test { public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("Hello World"); }}
在上面的例子中,Java程序初始類中的main()方法,將作為該程序初始線程的起點(diǎn),任何其他的線程都是由這個(gè)初始線程啟動(dòng)的。
在Java虛擬機(jī)內(nèi)部有兩種線程:守護(hù)線程和非守護(hù)線程。守護(hù)線程通常是由虛擬機(jī)自己使用的,比如執(zhí)行垃圾收集任務(wù)的線程。但是,Java程序也可以把它創(chuàng)建的任何線程標(biāo)記為守護(hù)線程。而Java程序中的初始線程——就是開(kāi)始于main()的那個(gè),是非守護(hù)線程。
只要還有任何非守護(hù)線程在運(yùn)行,那么這個(gè)Java程序也在繼續(xù)運(yùn)行。當(dāng)該程序中所有的非守護(hù)線程都終止時(shí),虛擬機(jī)實(shí)例將自動(dòng)退出。假若安全管理器允許,程序本身也能夠通過(guò)調(diào)用Runtime類或者System類的exit()方法來(lái)退出。
JAVA虛擬機(jī)的體系結(jié)構(gòu)
下圖是JAVA虛擬機(jī)的結(jié)構(gòu)圖,每個(gè)Java虛擬機(jī)都有一個(gè)類裝載子系統(tǒng),它根據(jù)給定的全限定名來(lái)裝入類型(類或接口)。同樣,每個(gè)Java虛擬機(jī)都有一個(gè)執(zhí)行引擎,它負(fù)責(zé)執(zhí)行那些包含在被裝載類的方法中的指令。
當(dāng)JAVA虛擬機(jī)運(yùn)行一個(gè)程序時(shí),它需要內(nèi)存來(lái)存儲(chǔ)許多東西,例如:字節(jié)碼、從已裝載的class文件中得到的其他信息、程序創(chuàng)建的對(duì)象、傳遞給方法的參數(shù),返回值、局部變量等等。Java虛擬機(jī)把這些東西都組織到幾個(gè)“運(yùn)行時(shí)數(shù)據(jù)區(qū)”中,以便于管理。
某些運(yùn)行時(shí)數(shù)據(jù)區(qū)是由程序中所有線程共享的,還有一些則只能由一個(gè)線程擁有。每個(gè)Java虛擬機(jī)實(shí)例都有一個(gè)方法區(qū)以及一個(gè)堆,它們是由該虛擬機(jī)實(shí)例中所有的線程共享的。當(dāng)虛擬機(jī)裝載一個(gè)class文件時(shí),它會(huì)從這個(gè)class文件包含的二進(jìn)制數(shù)據(jù)中解析類型信息。然后把這些類型信息放到方法區(qū)中。當(dāng)程序運(yùn)行時(shí),虛擬機(jī)會(huì)把所有該程序在運(yùn)行時(shí)創(chuàng)建的對(duì)象都放到堆中。
當(dāng)每一個(gè)新線程被創(chuàng)建時(shí),它都將得到它自己的PC寄存器(程序計(jì)數(shù)器)以及一個(gè)Java棧,如果線程正在執(zhí)行的是一個(gè)Java方法(非本地方法),那么PC寄存器的值將總是指向下一條將被執(zhí)行的指令,而它的Java棧則總是存儲(chǔ)該線程中Java方法調(diào)用的狀態(tài)——包括它的局部變量,被調(diào)用時(shí)傳進(jìn)來(lái)的參數(shù)、返回值,以及運(yùn)算的中間結(jié)果等等。而本地方法調(diào)用的狀態(tài),則是以某種依賴于具體實(shí)現(xiàn)的方法存儲(chǔ)在本地方法棧中,也可能是在寄存器或者其他某些與特定實(shí)現(xiàn)相關(guān)的內(nèi)存區(qū)中。
Java棧是由許多棧幀(stack frame)組成的,一個(gè)棧幀包含一個(gè)Java方法調(diào)用的狀態(tài)。當(dāng)線程調(diào)用一個(gè)Java方法時(shí),虛擬機(jī)壓入一個(gè)新的棧幀到該線程的Java棧中,當(dāng)該方法返回時(shí),這個(gè)棧幀被從Java棧中彈出并拋棄。
Java虛擬機(jī)沒(méi)有寄存器,其指令集使用Java棧來(lái)存儲(chǔ)中間數(shù)據(jù)。這樣設(shè)計(jì)的原因是為了保持Java虛擬機(jī)的指令集盡量緊湊、同時(shí)也便于Java虛擬機(jī)在那些只有很少通用寄存器的平臺(tái)上實(shí)現(xiàn)。另外,Java虛擬機(jī)這種基于棧的體系結(jié)構(gòu),也有助于運(yùn)行時(shí)某些虛擬機(jī)實(shí)現(xiàn)的動(dòng)態(tài)編譯器和即時(shí)編譯器的代碼優(yōu)化。
下圖描繪了Java虛擬機(jī)為每一個(gè)線程創(chuàng)建的內(nèi)存區(qū),這些內(nèi)存區(qū)域是私有的,任何線程都不能訪問(wèn)另一個(gè)線程的PC寄存器或者Java棧。
上圖展示了一個(gè)虛擬機(jī)實(shí)例的快照,它有三個(gè)線程正在執(zhí)行。線程1和線程2都正在執(zhí)行Java方法,而線程3則正在執(zhí)行一個(gè)本地方法。
Java棧都是向下生長(zhǎng)的,而棧頂都顯示在圖的底部。當(dāng)前正在執(zhí)行的方法的棧幀則以淺色表示,對(duì)于一個(gè)正在運(yùn)行Java方法的線程而言,它的PC寄存器總是指向下一條將被執(zhí)行的指令。比如線程1和線程2都是以淺色顯示的,由于線程3當(dāng)前正在執(zhí)行一個(gè)本地方法,因此,它的PC寄存器——以深色顯示的那個(gè),其值是不確定的。
數(shù)據(jù)類型 Java虛擬機(jī)是通過(guò)某些數(shù)據(jù)類型來(lái)執(zhí)行計(jì)算的,數(shù)據(jù)類型可以分為兩種:基本類型和引用類型,基本類型的變量持有原始值,而引用類型的變量持有引用值。
Java語(yǔ)言中的所有基本類型同樣也都是Java虛擬機(jī)中的基本類型。但是boolean有點(diǎn)特別,雖然Java虛擬機(jī)也把boolean看做基本類型,但是指令集對(duì)boolean只有很有限的支持,當(dāng)編譯器把Java源代碼編譯為字節(jié)碼時(shí),它會(huì)用int或者byte來(lái)表示boolean。在Java虛擬機(jī)中,false是由整數(shù)零來(lái)表示的,所有非零整數(shù)都表示true,涉及boolean值的操作則會(huì)使用int。另外,boolean數(shù)組是當(dāng)做byte數(shù)組來(lái)訪問(wèn)的,但是在“堆”區(qū),它也可以被表示為位域。
Java虛擬機(jī)還有一個(gè)只在內(nèi)部使用的基本類型:returnAddress,Java程序員不能使用這個(gè)類型,這個(gè)基本類型被用來(lái)實(shí)現(xiàn)Java程序中的finally子句。該類型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作碼的指針。returnAddress類型不是簡(jiǎn)單意義上的數(shù)值,不屬于任何一種基本類型,并且它的值是不能被運(yùn)行中的程序所修改的。
Java虛擬機(jī)的引用類型被統(tǒng)稱為“引用(reference)”,有三種引用類型:類類型、接口類型、以及數(shù)組類型,它們的值都是對(duì)動(dòng)態(tài)創(chuàng)建對(duì)象的引用。類類型的值是對(duì)類實(shí)例的引用;數(shù)組類型的值是對(duì)數(shù)組對(duì)象的引用,在Java虛擬機(jī)中,數(shù)組是個(gè)真正的對(duì)象;而接口類型的值,則是對(duì)實(shí)現(xiàn)了該接口的某個(gè)類實(shí)例的引用。還有一種特殊的引用值是null,它表示該引用變量沒(méi)有引用任何對(duì)象。
JAVA中方法參數(shù)的引用傳遞
java中參數(shù)的傳遞有兩種,分別是按值傳遞和按引用傳遞。按值傳遞不必多說(shuō),下面就說(shuō)一下按引用傳遞。
“當(dāng)一個(gè)對(duì)象被當(dāng)作參數(shù)傳遞到一個(gè)方法”,這就是所謂的按引用傳遞。
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()); }}
上面代碼的輸出結(jié)果是“hello world”,這不必多說(shuō),那如果將set方法改為如下,結(jié)果會(huì)是多少呢?
public void set(User user){ user.setName("hello world"); user = new User(); user.setName("change"); }
答案依然是“hello world”,下面就讓我們來(lái)分析一下如上代碼。
首先
User user = new User();
是在堆中創(chuàng)建了一個(gè)對(duì)象,并在棧中創(chuàng)建了一個(gè)引用,此引用指向該對(duì)象,如下圖:
test.set(user);
是將引用user作為參數(shù)傳遞到set方法,注意:這里傳遞的并不是引用本身,而是一個(gè)引用的拷貝。也就是說(shuō)這時(shí)有兩個(gè)引用(引用和引用的拷貝)同時(shí)指向堆中的對(duì)象,如下圖:
user.setName("hello world");
在set()方法中,“user引用的拷貝”操作堆中的User對(duì)象,給name屬性設(shè)置字符串”hello world”。如下圖:
user = new User();
在set()方法中,又創(chuàng)建了一個(gè)User對(duì)象,并將“user引用的拷貝”指向這個(gè)在堆中新創(chuàng)建的對(duì)象,如下圖:
user.setName("change");
在set()方法中,“user引用的拷貝”操作的是堆中新創(chuàng)建的User對(duì)象。
set()方法執(zhí)行完畢,目光再回到mian()方法 System.out.println(user.getName());
因?yàn)橹?,”user引用的拷貝”已經(jīng)將堆中的User對(duì)象的name屬性設(shè)置為了”hello world”,所以當(dāng)main()方法中的user調(diào)用getName()時(shí),打印的結(jié)果就是”hello world”。如下圖:
類裝載子系統(tǒng)
在JAVA虛擬機(jī)中,負(fù)責(zé)查找并裝載類型的那部分被稱為類裝載子系統(tǒng)。
JAVA虛擬機(jī)有兩種類裝載器:?jiǎn)?dòng)類裝載器和用戶自定義類裝載器。前者是JAVA虛擬機(jī)實(shí)現(xiàn)的一部分,后者則是Java程序的一部分。由不同的類裝載器裝載的類將被放在虛擬機(jī)內(nèi)部的不同命名空間中。
類裝載器子系統(tǒng)涉及Java虛擬機(jī)的其他幾個(gè)組成部分,以及幾個(gè)來(lái)自java.lang庫(kù)的類。比如,用戶自定義的類裝載器是普通的Java對(duì)象,它的類必須派生自java.lang.ClassLoader類。ClassLoader中定義的方法為程序提供了訪問(wèn)類裝載器機(jī)制的接口。此外,對(duì)于每一個(gè)被裝載的類型,JAVA虛擬機(jī)都會(huì)為它創(chuàng)建一個(gè)java.lang.Class類的實(shí)例來(lái)代表該類型。和所有其他對(duì)象一樣,用戶自定義的類裝載器以及Class類的實(shí)例都放在內(nèi)存中的堆區(qū),而裝載的類型信息則都位于方法區(qū)。
類裝載器子系統(tǒng)除了要定位和導(dǎo)入二進(jìn)制class文件外,還必須負(fù)責(zé)驗(yàn)證被導(dǎo)入類的正確性,為類變量分配并初始化內(nèi)存,以及幫助解析符號(hào)引用。這些動(dòng)作必須嚴(yán)格按以下順序進(jìn)行:
(1)裝載——查找并裝載類型的二進(jìn)制數(shù)據(jù)。
(2)連接——指向驗(yàn)證、準(zhǔn)備、以及解析(可選)。
● 驗(yàn)證 確保被導(dǎo)入類型的正確性。
● 準(zhǔn)備 為類變量分配內(nèi)存,并將其初始化為默認(rèn)值。
● 解析 把類型中的符號(hào)引用轉(zhuǎn)換為直接引用。
(3)初始化——把類變量初始化為正確初始值。
每個(gè)JAVA虛擬機(jī)實(shí)現(xiàn)都必須有一個(gè)啟動(dòng)類裝載器,它知道怎么裝載受信任的類。
每個(gè)類裝載器都有自己的命名空間,其中維護(hù)著由它裝載的類型。所以一個(gè)Java程序可以多次裝載具有同一個(gè)全限定名的多個(gè)類型。這樣一個(gè)類型的全限定名就不足以確定在一個(gè)Java虛擬機(jī)中的唯一性。因此,當(dāng)多個(gè)類裝載器都裝載了同名的類型時(shí),為了惟一地標(biāo)識(shí)該類型,還要在類型名稱前加上裝載該類型(指出它所位于的命名空間)的類裝載器標(biāo)識(shí)。
方法區(qū)
在Java虛擬機(jī)中,關(guān)于被裝載類型的信息存儲(chǔ)在一個(gè)邏輯上被稱為方法區(qū)的內(nèi)存中。當(dāng)虛擬機(jī)裝載某個(gè)類型時(shí),它使用類裝載器定位相應(yīng)的class文件,然后讀入這個(gè)class文件——1個(gè)線性二進(jìn)制數(shù)據(jù)流,然后它傳輸?shù)教摂M機(jī)中,緊接著虛擬機(jī)提取其中的類型信息,并將這些信息存儲(chǔ)到方法區(qū)。該類型中的類(靜態(tài))變量同樣也是存儲(chǔ)在方法區(qū)中。
JAVA虛擬機(jī)在內(nèi)部如何存儲(chǔ)類型信息,這是由具體實(shí)現(xiàn)的設(shè)計(jì)者來(lái)決定的。
當(dāng)虛擬機(jī)運(yùn)行Java程序時(shí),它會(huì)查找使用存儲(chǔ)在方法區(qū)中的類型信息。由于所有線程都共享方法區(qū),因此它們對(duì)方法區(qū)數(shù)據(jù)的訪問(wèn)必須被設(shè)計(jì)為是線程安全的。比如,假設(shè)同時(shí)有兩個(gè)線程都企圖訪問(wèn)一個(gè)名為L(zhǎng)ava的類,而這個(gè)類還沒(méi)有被裝入虛擬機(jī),那么,這時(shí)只應(yīng)該有一個(gè)線程去裝載它,而另一個(gè)線程則只能等待。
對(duì)于每個(gè)裝載的類型,虛擬機(jī)都會(huì)在方法區(qū)中存儲(chǔ)以下類型信息:
● 這個(gè)類型的全限定名
● 這個(gè)類型的直接超類的全限定名(除非這個(gè)類型是java.lang.Object,它沒(méi)有超類)
● 這個(gè)類型是類類型還是接口類型
● 這個(gè)類型的訪問(wèn)修飾符(public、abstract或final的某個(gè)子集)
● 任何直接超接口的全限定名的有序列表
除了上面列出的基本類型信息外,虛擬機(jī)還得為每個(gè)被裝載的類型存儲(chǔ)以下信息:
● 該類型的常量池
● 字段信息
● 方法信息
● 除了常量以外的所有類(靜態(tài))變量
● 一個(gè)到類ClassLoader的引用
● 一個(gè)到Class類的引用
常量池
虛擬機(jī)必須為每個(gè)被裝載的類型維護(hù)一個(gè)常量池。常量池就是該類型所用常量的一個(gè)有序集合,包括直接常量和對(duì)其他類型、字段和方法的符號(hào)引用。池中的數(shù)據(jù)項(xiàng)就像數(shù)組一樣是通過(guò)索引訪問(wèn)的。因?yàn)槌A砍卮鎯?chǔ)了相應(yīng)類型所用到的所有類型、字段和方法的符號(hào)引用,所以它在Java程序的動(dòng)態(tài)連接中起著核心的作用。
字段信息
對(duì)于類型中聲明的每一個(gè)字段。方法區(qū)中必須保存下面的信息。除此之外,這些字段在類或者接口中的聲明順序也必須保存。
○ 字段名
○ 字段的類型
○ 字段的修飾符(public、private、protected、static、final、volatile、transient的某個(gè)子集)
方法信息
對(duì)于類型中聲明的每一個(gè)方法,方法區(qū)中必須保存下面的信息。和字段一樣,這些方法在類或者接口中的聲明順序也必須保存。
○ 方法名
○ 方法的返回類型(或void)
○ 方法參數(shù)的數(shù)量和類型(按聲明順序)
○ 方法的修飾符(public、private、protected、static、final、synchronized、native、abstract的某個(gè)子集)
除了上面清單中列出的條目之外,如果某個(gè)方法不是抽象的和本地的,它還必須保存下列信息:
○ 方法的字節(jié)碼(bytecodes)
○ 操作數(shù)棧和該方法的棧幀中的局部變量區(qū)的大小
○ 異常表
類(靜態(tài))變量
類變量是由所有類實(shí)例共享的,但是即使沒(méi)有任何類實(shí)例,它也可以被訪問(wèn)。這些變量只與類有關(guān)——而非類的實(shí)例,因此它們總是作為類型信息的一部分而存儲(chǔ)在方法區(qū)。除了在類中聲明的編譯時(shí)常量外,虛擬機(jī)在使用某個(gè)類之前,必須在方法區(qū)中為這些類變量分配空間。
而編譯時(shí)常量(就是那些用final聲明以及用編譯時(shí)已知的值初始化的類變量)則和一般的類變量處理方式不同,每個(gè)使用編譯時(shí)常量的類型都會(huì)復(fù)制它的所有常量到自己的常量池中,或嵌入到它的字節(jié)碼流中。作為常量池或字節(jié)碼流的一部分,編譯時(shí)常量保存在方法區(qū)中——就和一般的類變量一樣。但是當(dāng)一般的類變量作為聲明它們的類型的一部分?jǐn)?shù)據(jù)面保存的時(shí)候,編譯時(shí)常量作為使用它們的類型的一部分而保存。
指向ClassLoader類的引用
每個(gè)類型被裝載的時(shí)候,虛擬機(jī)必須跟蹤它是由啟動(dòng)類裝載器還是由用戶自定義類裝載器裝載的。如果是用戶自定義類裝載器裝載的,那么虛擬機(jī)必須在類型信息中存儲(chǔ)對(duì)該裝載器的引用。這是作為方法表中的類型數(shù)據(jù)的一部分保存的。
虛擬機(jī)會(huì)在動(dòng)態(tài)連接期間使用這個(gè)信息。當(dāng)某個(gè)類型引用另一個(gè)類型的時(shí)候,虛擬機(jī)會(huì)請(qǐng)求裝載發(fā)起引用類型的類裝載器來(lái)裝載被引用的類型。這個(gè)動(dòng)態(tài)連接的過(guò)程,對(duì)于虛擬機(jī)分離命名空間的方式也是至關(guān)重要的。為了能夠正確地執(zhí)行動(dòng)態(tài)連接以及維護(hù)多個(gè)命名空間,虛擬機(jī)需要在方法表中得知每個(gè)類都是由哪個(gè)類裝載器裝載的。
指向Class類的引用
對(duì)于每一個(gè)被裝載的類型(不管是類還是接口),虛擬機(jī)都會(huì)相應(yīng)地為它創(chuàng)建一個(gè)java.lang.Class類的實(shí)例,而且虛擬機(jī)還必須以某種方式把這個(gè)實(shí)例和存儲(chǔ)在方法區(qū)中的類型數(shù)據(jù)關(guān)聯(lián)起來(lái)。
在Java程序中,你可以得到并使用指向Class對(duì)象的引用。Class類中的一個(gè)靜態(tài)方法可以讓用戶得到任何已裝載的類的Class實(shí)例的引用。
public static Class
比如,如果調(diào)用forName(“java.lang.Object”),那么將得到一個(gè)代表java.lang.Object的Class對(duì)象的引用。可以使用forName()來(lái)得到代表任何包中任何類型的Class對(duì)象的引用,只要這個(gè)類型可以被(或者已經(jīng)被)裝載到當(dāng)前命名空間中。如果虛擬機(jī)無(wú)法把請(qǐng)求的類型裝載到當(dāng)前命名空間,那么會(huì)拋出ClassNotFoundException異常。
另一個(gè)得到Class對(duì)象引用的方法是,可以調(diào)用任何對(duì)象引用的getClass()方法。這個(gè)方法被來(lái)自O(shè)bject類本身的所有對(duì)象繼承:
public final native Class
比如,如果你有一個(gè)到j(luò)ava.lang.Integer類的對(duì)象的引用,那么你只需簡(jiǎn)單地調(diào)用Integer對(duì)象引用的getClass()方法,就可以得到表示java.lang.Integer類的Class對(duì)象。
方法區(qū)使用實(shí)例
為了展示虛擬機(jī)如何使用方法區(qū)中的信息,下面來(lái)舉例說(shuō)明:
class Lava { private int speed = 5; void flow(){ }}public class Volcano { public static void main(String[] args){ Lava lava = new Lava(); lava.flow(); }}
不同的虛擬機(jī)實(shí)現(xiàn)可能會(huì)用完全不同的方法來(lái)操作,下面描述的只是其中一種可能——但并不是僅有的一種。
要運(yùn)行Volcano程序,首先得以某種“依賴于實(shí)現(xiàn)的”方式告訴虛擬機(jī)“Volcano”這個(gè)名字。之后,虛擬機(jī)將找到并讀入相應(yīng)的class文件“Volcano.class”,然后它會(huì)從導(dǎo)入的class文件里的二進(jìn)制數(shù)據(jù)中提取類型信息并放到方法區(qū)中。通過(guò)執(zhí)行保存在方法區(qū)中的字節(jié)碼,虛擬機(jī)開(kāi)始執(zhí)行main()方法,在執(zhí)行時(shí),它會(huì)一直持有指向當(dāng)前類(Volcano類)的常量池(方法區(qū)中的一個(gè)數(shù)據(jù)結(jié)構(gòu))的指針。
注意:虛擬機(jī)開(kāi)始執(zhí)行Volcano類中main()方法的字節(jié)碼的時(shí)候,盡管Lava類還沒(méi)被裝載,但是和大多數(shù)(也許所有)虛擬機(jī)實(shí)現(xiàn)一樣,它不會(huì)等到把程序中用到的所有類都裝載后才開(kāi)始運(yùn)行。恰好相反,它只會(huì)需要時(shí)才裝載相應(yīng)的類。
main()的第一條指令告知虛擬機(jī)為列在常量池第一項(xiàng)的類分配足夠的內(nèi)存。所以虛擬機(jī)使用指向Volcano常量池的指針找到第一項(xiàng),發(fā)現(xiàn)它是一個(gè)對(duì)Lava類的符號(hào)引用,然后它就檢查方法區(qū),看Lava類是否已經(jīng)被加載了。
這個(gè)符號(hào)引用僅僅是一個(gè)給出了類Lava的全限定名“Lava”的字符串。為了能讓虛擬機(jī)盡可能快地從一個(gè)名稱找到類,虛擬機(jī)的設(shè)計(jì)者應(yīng)當(dāng)選擇最佳的數(shù)據(jù)結(jié)構(gòu)和算法。
當(dāng)虛擬機(jī)發(fā)現(xiàn)還沒(méi)有裝載過(guò)名為“Lava”的類時(shí),它就開(kāi)始查找并裝載文件“Lava.class”,并把從讀入的二進(jìn)制數(shù)據(jù)中提取的類型信息放在方法區(qū)中。
緊接著,虛擬機(jī)以一個(gè)直接指向方法區(qū)Lava類數(shù)據(jù)的指針來(lái)替換常量池第一項(xiàng)(就是那個(gè)字符串“Lava”),以后就可以用這個(gè)指針來(lái)快速地訪問(wèn)Lava類了。這個(gè)替換過(guò)程稱為常量池解析,即把常量池中的符號(hào)引用替換為直接引用。
終于,虛擬機(jī)準(zhǔn)備為一個(gè)新的Lava對(duì)象分配內(nèi)存。此時(shí)它又需要方法區(qū)中的信息。還記得剛剛放到Volcano類常量池第一項(xiàng)的指針嗎?現(xiàn)在虛擬機(jī)用它來(lái)訪問(wèn)Lava類型信息,找出其中記錄的這樣一條信息:一個(gè)Lava對(duì)象需要分配多少堆空間。
JAVA虛擬機(jī)總能夠通過(guò)存儲(chǔ)與方法區(qū)的類型信息來(lái)確定一個(gè)對(duì)象需要多少內(nèi)存,當(dāng)JAVA虛擬機(jī)確定了一個(gè)Lava對(duì)象的大小后,它就在堆上分配這么大的空間,并把這個(gè)對(duì)象實(shí)例的變量speed初始化為默認(rèn)初始值0。
當(dāng)把新生成的Lava對(duì)象的引用壓到棧中,main()方法的第一條指令也完成了。接下來(lái)的指令通過(guò)這個(gè)引用調(diào)用Java代碼(該代碼把speed變量初始化為正確初始值5)。另一條指令將用這個(gè)引用調(diào)用Lava對(duì)象引用的flow()方法。
堆
Java程序在運(yùn)行時(shí)創(chuàng)建的所有類實(shí)例或數(shù)組都放在同一個(gè)堆中。而一個(gè)JAVA虛擬機(jī)實(shí)例中只存在一個(gè)堆空間,因此所有線程都將共享這個(gè)堆。又由于一個(gè)Java程序獨(dú)占一個(gè)JAVA虛擬機(jī)實(shí)例,因而每個(gè)Java程序都有它自己的堆空間——它們不會(huì)彼此干擾。但是同一個(gè)Java程序的多個(gè)線程卻共享著同一個(gè)堆空間,在這種情況下,就得考慮多線程訪問(wèn)對(duì)象(堆數(shù)據(jù))的同步問(wèn)題了。
JAVA虛擬機(jī)有一條在堆中分配新對(duì)象的指令,卻沒(méi)有釋放內(nèi)存的指令,正如你無(wú)法用Java代碼區(qū)明確釋放一個(gè)對(duì)象一樣。虛擬機(jī)自己負(fù)責(zé)決定如何以及何時(shí)釋放不再被運(yùn)行的程序引用的對(duì)象所占據(jù)的內(nèi)存。通常,虛擬機(jī)把這個(gè)任務(wù)交給垃圾收集器。
數(shù)組的內(nèi)部表示
在Java中,數(shù)組是真正的對(duì)象。和其他對(duì)象一樣,數(shù)組總是存儲(chǔ)在堆中。同樣,數(shù)組也擁有一個(gè)與它們的類相關(guān)聯(lián)的Class實(shí)例,所有具有相同維度和類型的數(shù)組都是同一個(gè)類的實(shí)例,而不管數(shù)組的長(zhǎng)度(多維數(shù)組每一維的長(zhǎng)度)是多少。例如一個(gè)包含3個(gè)int整數(shù)的數(shù)組和一個(gè)包含300個(gè)整數(shù)的數(shù)組擁有同一個(gè)類。數(shù)組的長(zhǎng)度只與實(shí)例數(shù)據(jù)有關(guān)。
數(shù)組類的名稱由兩部分組成:每一維用一個(gè)方括號(hào)“[”表示,用字符或字符串表示元素類型。比如,元素類型為int整數(shù)的、一維數(shù)組的類名為“[I”,元素類型為byte的三維數(shù)組為“[[[B”,元素類型為Object的二維數(shù)組為“[[Ljava/lang/Object”。
多維數(shù)組被表示為數(shù)組的數(shù)組。比如,int類型的二維數(shù)組,將表示為一個(gè)一維數(shù)組,其中的每一個(gè)元素是一個(gè)一維int數(shù)組的引用,如下圖:
在堆中的每個(gè)數(shù)組對(duì)象還必須保存的數(shù)據(jù)時(shí)數(shù)組的長(zhǎng)度、數(shù)組數(shù)據(jù),以及某些指向數(shù)組的類數(shù)據(jù)的引用。虛擬機(jī)必須能夠通過(guò)一個(gè)數(shù)組對(duì)象的引用得到此數(shù)組的長(zhǎng)度,通過(guò)索引訪問(wèn)其元素(期間要檢查數(shù)組邊界是否越界),調(diào)用所有數(shù)組的直接超類Object聲明的方法等等。
程序計(jì)數(shù)器
對(duì)于一個(gè)運(yùn)行中的Java程序而言,其中的每一個(gè)線程都有它自己的PC(程序計(jì)數(shù)器)寄存器,它是在該線程啟動(dòng)時(shí)創(chuàng)建的,PC寄存器的大小是一個(gè)字長(zhǎng),因此它既能夠持有一個(gè)本地指針,也能夠持有一個(gè)returnAddress。當(dāng)線程執(zhí)行某個(gè)Java方法時(shí),PC寄存器的內(nèi)容總是下一條將被執(zhí)行指令的“地址”,這里的“地址”可以是一個(gè)本地指針,也可以是在方法字節(jié)碼中相對(duì)于該方法起始指令的偏移量。如果該線程正在執(zhí)行一個(gè)本地方法,那么此時(shí)PC寄存器的值是“undefined”。
Java棧
每當(dāng)啟動(dòng)一個(gè)新線程時(shí),Java虛擬機(jī)都會(huì)為它分配一個(gè)Java棧。Java棧以幀為單位保存線程的運(yùn)行狀態(tài)。虛擬機(jī)只會(huì)直接對(duì)Java棧執(zhí)行兩種操作:以幀為單位的壓棧和出棧。
某個(gè)線程正在執(zhí)行的方法被稱為該線程的當(dāng)前方法,當(dāng)前方法使用的棧幀稱為當(dāng)前幀,當(dāng)前方法所屬的類稱為當(dāng)前類,當(dāng)前類的常量池稱為當(dāng)前常量池。在線程執(zhí)行一個(gè)方法時(shí),它會(huì)跟蹤當(dāng)前類和當(dāng)前常量池。此外,當(dāng)虛擬機(jī)遇到棧內(nèi)操作指令時(shí),它對(duì)當(dāng)前幀內(nèi)數(shù)據(jù)執(zhí)行操作。
每當(dāng)線程調(diào)用一個(gè)Java方法時(shí),虛擬機(jī)都會(huì)在該線程的Java棧中壓入一個(gè)新幀。而這個(gè)新幀自然就成為了當(dāng)前幀。在執(zhí)行這個(gè)方法時(shí),它使用這個(gè)幀來(lái)存儲(chǔ)參數(shù)、局部變量、中間運(yùn)算結(jié)果等數(shù)據(jù)。
Java方法可以以兩種方式完成。一種通過(guò)return返回的,稱為正常返回;一種是通過(guò)拋出異常而異常終止的。不管以哪種方式返回,虛擬機(jī)都會(huì)將當(dāng)前幀彈出Java棧然后釋放掉,這樣上一個(gè)方法的幀就成為當(dāng)前幀了。
Java幀上的所有數(shù)據(jù)都是此線程私有的。任何線程都不能訪問(wèn)另一個(gè)線程的棧數(shù)據(jù),因此我們不需要考慮多線程情況下棧數(shù)據(jù)的訪問(wèn)同步問(wèn)題。當(dāng)一個(gè)線程調(diào)用一個(gè)方法時(shí),方法的的局部變量保存在調(diào)用線程Java棧的幀中。只有一個(gè)線程能總是訪問(wèn)那些局部變量,即調(diào)用方法的線程。
本地方法棧
前面提到的所有運(yùn)行時(shí)數(shù)據(jù)區(qū)都是Java虛擬機(jī)規(guī)范中明確定義的,除此之外,對(duì)于一個(gè)運(yùn)行中的Java程序而言,它還可能會(huì)用到一些跟本地方法相關(guān)的數(shù)據(jù)區(qū)。當(dāng)某個(gè)線程調(diào)用一個(gè)本地方法時(shí),它就進(jìn)入了一個(gè)全新的并且不再受虛擬機(jī)限制的世界。本地方法可以通過(guò)本地方法接口來(lái)訪問(wèn)虛擬機(jī)的運(yùn)行時(shí)數(shù)據(jù)區(qū),但不止如此,它還可以做任何它想做的事情。
本地方法本質(zhì)上時(shí)依賴于實(shí)現(xiàn)的,虛擬機(jī)實(shí)現(xiàn)的設(shè)計(jì)者們可以自由地決定使用怎樣的機(jī)制來(lái)讓Java程序調(diào)用本地方法。
任何本地方法接口都會(huì)使用某種本地方法棧。當(dāng)線程調(diào)用Java方法時(shí),虛擬機(jī)會(huì)創(chuàng)建一個(gè)新的棧幀并壓入Java棧。然而當(dāng)它調(diào)用的是本地方法時(shí),虛擬機(jī)會(huì)保持Java棧不變,不再在線程的Java棧中壓入新的幀,虛擬機(jī)只是簡(jiǎn)單地動(dòng)態(tài)連接并直接調(diào)用指定的本地方法。
如果某個(gè)虛擬機(jī)實(shí)現(xiàn)的本地方法接口是使用C連接模型的話,那么它的本地方法棧就是C棧。當(dāng)C程序調(diào)用一個(gè)C函數(shù)時(shí),其棧操作都是確定的。傳遞給該函數(shù)的參數(shù)以某個(gè)確定的順序壓入棧,它的返回值也以確定的方式傳回調(diào)用者。同樣,這就是虛擬機(jī)實(shí)現(xiàn)中本地方法棧的行為。
很可能本地方法接口需要回調(diào)Java虛擬機(jī)中的Java方法,在這種情況下,該線程會(huì)保存本地方法棧的狀態(tài)并進(jìn)入到另一個(gè)Java棧。
下圖描繪了這樣一個(gè)情景,就是當(dāng)一個(gè)線程調(diào)用一個(gè)本地方法時(shí),本地方法又回調(diào)虛擬機(jī)中的另一個(gè)Java方法。這幅圖展示了JAVA虛擬機(jī)內(nèi)部線程運(yùn)行的全景圖。一個(gè)線程可能在整個(gè)生命周期中都執(zhí)行Java方法,操作它的Java棧;或者它可能毫無(wú)障礙地在Java棧和本地方法棧之間跳轉(zhuǎn)?! ?br>
該線程首先調(diào)用了兩個(gè)Java方法,而第二個(gè)Java方法又調(diào)用了一個(gè)本地方法,這樣導(dǎo)致虛擬機(jī)使用了一個(gè)本地方法棧。假設(shè)這是一個(gè)C語(yǔ)言棧,其間有兩個(gè)C函數(shù),第一個(gè)C函數(shù)被第二個(gè)Java方法當(dāng)做本地方法調(diào)用,而這個(gè)C函數(shù)又調(diào)用了第二個(gè)C函數(shù)。之后第二個(gè)C函數(shù)又通過(guò)本地方法接口回調(diào)了一個(gè)Java方法(第三個(gè)Java方法),最終這個(gè)Java方法又調(diào)用了一個(gè)Java方法(它成為圖中的當(dāng)前方法)。
出處:Java虛擬機(jī)體系結(jié)構(gòu)
關(guān)鍵詞:結(jié)構(gòu),虛擬,機(jī)體