- 深入解析Android 虛擬機
- 鐘世禮
- 6271字
- 2019-01-05 00:54:35
2.2 Java虛擬機概述
Java虛擬機(JVM)是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能模擬來實現(xiàn)的。Java虛擬機有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統(tǒng)。JVM虛擬機的運作結構如圖2-1所示。

圖2-1 JVM虛擬機的運作結構
從該圖中可以看到,JVM是運行在操作系統(tǒng)之上的,與硬件沒有直接的交互。JVM的具體組成部分如圖2-2所示。

圖2-2 JVM構成圖
(1)使用JVM的原因。
Java語言的一個非常重要的特點就是與平臺的無關性。而使用JVM是實現(xiàn)這一特點的關鍵。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。在引入JVM后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用模式JVM屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在JVM上運行的目標代碼(字節(jié)碼),就可以在多種平臺上不加修改地運行。當JVM執(zhí)行字節(jié)碼時,把字節(jié)碼解釋成具體平臺上的機器指令執(zhí)行。
(2)JVM的作用。
JVM是Java語言底層實現(xiàn)的基礎,對Java語言感興趣的讀者來說,很有必要對Java虛擬機有一個大概的了解。因為這不但有助于理解Java語言的一些性質,而且也有助于使用Java語言。對于要在特定平臺上實現(xiàn)JVM的軟件人員、Java語言的編譯器作者以及要用硬件芯片實現(xiàn)JVM的人員來說,必須深刻理解JVM的規(guī)范。另外,如果你想擴展Java語言,或是把其他語言編譯成Java語言的字節(jié)碼,你也需要深入地了解JVM。
在本節(jié)的內容中,將簡要講解和JVM相關的基本知識。
2.2.1 JVM的數(shù)據(jù)類型
在JVM機制中,可以支持如下所示的基本數(shù)據(jù)類型。
byte:1字節(jié)有符號整數(shù)的補碼。
short:2字節(jié)有符號整數(shù)的補碼。
int:4字節(jié)有符號整數(shù)的補碼。
long:8字節(jié)有符號整數(shù)的補碼。
float:4字節(jié)IEEE754單精度浮點數(shù)。
double:8字節(jié)IEEE754雙精度浮點數(shù)。
char:2字節(jié)無符號Unicode字符。
object:對一個Javaobject(對象)的4字節(jié)引用。
returnAddress:4字節(jié),用于jsr/ret/jsr-w/ret-w指令。
幾乎所有的Java類型檢查工作都是在編譯時完成的,上述列出的原始數(shù)據(jù)類型數(shù)據(jù)在Java執(zhí)行時不需要用硬件標記。操作這些原始數(shù)據(jù)類型數(shù)據(jù)的字節(jié)碼(指令)本身就已經指出了操作數(shù)的數(shù)據(jù)類型,例如iadd、ladd、fadd和dadd指令都是把兩個數(shù)相加,其操作數(shù)類型分別是int、long、float和double。虛擬機沒有給boolean(布爾)類型設置單獨的指令。boolean型的數(shù)據(jù)是由integer指令,包括integer返回來處理的。boolean型的數(shù)組則是用byte數(shù)組來處理的。虛擬機使用IEEE754格式的浮點數(shù),不支持IEEE格式的較舊的計算機,在運行Java數(shù)值計算程序時,可能會非常慢。
虛擬機的規(guī)范對于object內部的結構沒有任何特殊的要求。在Oracle公司的實現(xiàn)中,對object的引用是一個句柄,其中包含一對指針:一個指針指向該object的方法表,另一個指向該object的數(shù)據(jù)。用Java虛擬機的字節(jié)碼表示的程序應該遵守類型規(guī)定。Java虛擬機的實現(xiàn)應拒絕執(zhí)行違反了類型規(guī)定的字節(jié)碼程序。Java虛擬機由于字節(jié)碼定義的限制似乎只能運行于32位地址空間的機器上。但是可以創(chuàng)建一個Java虛擬機,它自動地把字節(jié)碼轉換成64位的形式。從Java虛擬機支持的數(shù)據(jù)類型可以看出,Java對數(shù)據(jù)類型的內部格式進行了嚴格規(guī)定,這樣使得各種Java虛擬機的實現(xiàn)對數(shù)據(jù)的解釋是相同的,從而保證了Java的與平臺無關性和可移植性。
2.2.2 Java虛擬機體系結構
JVM由如下5個部分組成。
一組指令集。
一組寄存器。
一個棧。
一個無用單元收集堆(Garbage-collected-heap)。
一個方法區(qū)域。
這5部分是Java虛擬機的邏輯成分,不依賴任何實現(xiàn)技術或組織方式,但它們的功能必須在真實機器上以某種方式實現(xiàn)。在接下來的內容中,將簡要介紹上述組成部分的基本知識,更加詳細的知識讀者可以參閱本書后面的內容。
1.Java指令集
Java虛擬機支持大約248個字節(jié)碼,每個字節(jié)碼執(zhí)行一種基本的CPU運算,例如把一個整數(shù)加到寄存器,子程序轉移等。Java指令集相當于Java程序的匯編語言。
Java指令集中的指令包含一個單字節(jié)的操作符,用于指定要執(zhí)行的操作,還有0個或多個操作數(shù),提供操作所需的參數(shù)或數(shù)據(jù)。許多指令沒有操作數(shù),僅由一個單字節(jié)的操作符構成。
虛擬機的內層循環(huán)的執(zhí)行過程如下:
do{ 取一個操作符字節(jié); 根據(jù)操作符的值執(zhí)行一個動作; }while(程序未結束)
由于指令系統(tǒng)的簡單性,使得虛擬機執(zhí)行的過程十分簡單,這樣有利于提高執(zhí)行的效率。指令中操作數(shù)的數(shù)量和大小是由操作符決定的。如果操作數(shù)比一個字節(jié)大,那么它存儲的順序是高位字節(jié)優(yōu)先。假如一個16位的參數(shù)存放時占用兩個字節(jié),其值為:
第一個字節(jié)*256+第二個字節(jié)
字節(jié)碼指令流一般只是字節(jié)對齊的,但是指令tabltch和lookup是例外,在這兩條指令內部要求強制的4字節(jié)邊界對齊。
2.寄存器
Java虛擬機的寄存器用于保存機器的運行狀態(tài),與微處理器中的某些專用寄存器類似,所有寄存器都是32位的。在Java虛擬機中有以下4種寄存器。
pc:Java程序計數(shù)器。
optop:指向操作數(shù)棧頂端的指針。
frame:指向當前執(zhí)行方法的執(zhí)行環(huán)境的指針。
vars:指向當前執(zhí)行方法的局部變量區(qū)第一個變量的指針。
Java虛擬機是棧式的,它不定義或使用寄存器來傳遞或接收參數(shù),其目的是為了保證指令集的簡潔性和實現(xiàn)時的高效性,特別是對于寄存器數(shù)目不多的處理器。
3.棧
Java虛擬機中的棧有3個區(qū)域,分別是局部變量區(qū)、運行環(huán)境區(qū)、操作數(shù)區(qū)。
(1)局部變量區(qū)。
每個Java方法使用一個固定大小的局部變量集。它們按照與vars寄存器的字偏移量來尋址。局部變量都是32位的。長整數(shù)和雙精度浮點數(shù)占據(jù)了兩個局部變量的空間,卻按照第一個局部變量的索引來尋址(例如,一個具有索引n的局部變量,如果是一個雙精度浮點數(shù),那么它實際占據(jù)了索引n和n+1所代表的存儲空間)。虛擬機規(guī)范并不要求在局部變量中的64位的值是64位對齊的。虛擬機提供了把局部變量中的值裝載到操作數(shù)棧的指令,也提供了把操作數(shù)棧中的值寫入局部變量的指令。
(2)運行環(huán)境區(qū)。
在運行環(huán)境中包含的信息可以實現(xiàn)動態(tài)鏈接、正常的方法返回和異常、錯誤傳播。
動態(tài)鏈接。
運行環(huán)境包括對指向當前類和當前方法的解釋器符號表的指針,用于支持方法代碼的動態(tài)鏈接。方法clas文件代碼在引用要調用的方法和要訪問的變量時使用符號。動態(tài)鏈接把符號形式的方法調用翻譯成實際方法調用,裝載必要的類以解釋還沒有定義的符號,并把變量訪問翻譯成與這些變量運行時的存儲結構相應的偏移地址。動態(tài)鏈接方法和變量使得方法中使用的其他類的變化不會影響到本程序的代碼。
正常的方法返回。
如果當前方法正常地結束了,在執(zhí)行了一條具有正確類型的返回指令時,調用的方法會得到一個返回值。執(zhí)行環(huán)境在正常返回的情況下用于恢復調用者的寄存器,并把調用者的程序計數(shù)器增加一個恰當?shù)臄?shù)值,以跳過已執(zhí)行過的方法調用指令,然后在調用者的執(zhí)行環(huán)境中繼續(xù)執(zhí)行下去。
異常和錯誤傳播。
異常情況在Java中被稱作Error(錯誤)或Exception(異常),是Throwable類的子類,在程序中的原因有如下兩點。
● 動態(tài)鏈接錯,如無法找到所需的class文件。
● 運行時出錯,如對一個空指針的引用程序使用了throw語句。當發(fā)生異常時,Java虛擬機采取如下措施解決。
檢查與當前方法相聯(lián)系的catch子句表。每個catch子句包含其有效指令范圍,能夠處理的異常類型,以及處理異常的代碼塊地址。
與異常相匹配的catch子句應該符合下面的條件:造成異常的指令在其指令范圍之內,發(fā)生的異常類型是其能處理的異常類型的子類型。如果找到了匹配的catch子句,那么系統(tǒng)轉移到指定的異常處理塊處執(zhí)行。如果沒有找到異常處理塊,重復尋找匹配的catch子句的過程,直到當前方法的所有嵌套的catch子句都被檢查過。
由于虛擬機從第一個匹配的catch子句處繼續(xù)執(zhí)行,所以catch子句表中的順序是很重要的。因為Java代碼是結構化的,因此總可以把某個方法的所有的異常處理器都按序排列到一個表中,對任意可能的程序計數(shù)器的值,都可以用線性的順序找到合適的異常處理塊,以處理在該程序計數(shù)器值下發(fā)生的異常情況。
如果找不到匹配的catch子句,那么當前方法得到一個"未截獲異常"的結果并返回到當前方法的調用者,好像異常剛剛在其調用者中發(fā)生一樣。如果在調用者中仍然沒有找到相應的異常處理塊,那么這種錯誤傳播將被繼續(xù)下去。如果錯誤被傳播到最頂層,那么系統(tǒng)將調用一個缺省的異常處理塊。
(3)操作數(shù)棧區(qū)。
機器指令只從操作數(shù)棧中取操作數(shù),對它們進行操作,并把結果返回到棧中。選擇棧結構的原因是:在只有少量寄存器或非通用寄存器的機器(如Intel486)上,也能夠高效地模擬虛擬機的行為。操作數(shù)棧是32位的。它用于給方法傳遞參數(shù),并從方法接收結果,也用于支持操作的參數(shù),并保存操作的結果。例如,iadd指令將兩個整數(shù)相加,相加的兩個整數(shù)應該是操作數(shù)棧頂?shù)膬蓚€字,這兩個字是由先前的指令壓進堆棧的,這兩個整數(shù)將從堆棧彈出、相加,并把結果壓回到操作數(shù)棧中。
每個原始數(shù)據(jù)類型都有專門的指令對它們進行必須的操作。每個操作數(shù)在棧中需要一個存儲位置,除了long和double型,它們需要兩個位置。操作數(shù)只能被適用于其類型的操作符所操作。例如壓入兩個int類型的數(shù),如果把它們當作是一個long類型的數(shù)則是非法的。在Sun的虛擬機實現(xiàn)中,這個限制由字節(jié)碼驗證器強制實行。但是有少數(shù)操作(操作符dupe和swap),用于對運行時數(shù)據(jù)區(qū)進行操作時是不考慮類型的。
4.無用單元收集堆
Java的堆是一個運行時數(shù)據(jù)區(qū),類的實例(對象)從中分配空間。Java語言具有無用單元收集能力,即它不給程序員顯示釋放對象的能力。Java不規(guī)定具體使用的無用單元收集算法,可以根據(jù)系統(tǒng)的需求使用各種各樣的算法。
5.方法區(qū)
方法區(qū)與傳統(tǒng)語言中的編譯后代碼或是Unix進程中的正文段類似。它保存方法代碼(編譯后的java代碼)和符號表。在當前的Java實現(xiàn)中,方法代碼不包括在無用單元收集堆中,但計劃在將來的版本中實現(xiàn)。每個類文件包含了一個Java類或一個Java界面的編譯后的代碼。可以說類文件是Java語言的執(zhí)行代碼文件。為了保證類文件的平臺無關性,Java虛擬機規(guī)范中對類文件的格式也作了詳細的說明。其具體細節(jié)請參考Sun公司的Java虛擬機規(guī)范。
在Java虛擬機規(guī)范中,一個虛擬機實例的行為是分別按照子系統(tǒng)、內存區(qū)、數(shù)據(jù)類型以及指令這幾個術語來描述的。這些組成部分一起展示了抽象的虛擬機的內部抽象體系結構。但是規(guī)范中對它們的定義并非要強制規(guī)定Java虛擬機實現(xiàn)內部的體系結構,更多的是為了嚴格地定義這些實現(xiàn)的外部特征。規(guī)范本身通過定義這些抽象的組成部分以及它們之間的交互,來定義任何Java虛擬機實現(xiàn)都必須遵守的行為。
圖2-3是Java虛擬機的結構框圖,包括在規(guī)范中描述的主要子系統(tǒng)和內存區(qū)。前一章曾提到,每個Java虛擬機都有一個類裝載器子系統(tǒng),它根據(jù)給定的全限定名類裝入類型(類或接口),同樣,每個Java虛擬機都有一個執(zhí)行引擎,它負責執(zhí)行那些包含在被裝載類的方法中的指令。

圖2-3 Java虛擬機的內部體系結構
當Java虛擬機運行一個程序時,它需要使用內存來存儲許多東西,例如下面所示的元素。
字節(jié)碼。
從已裝載的class文件中得到的其他信息。
程序創(chuàng)建的對象。
傳遞給方法的參數(shù)。
返回值。
局部變量。
運算的中間結果。
Java虛擬機會把上述元素都組織到幾個“運行時數(shù)據(jù)區(qū)”中,目的是便于管理。盡管這些“運行時數(shù)據(jù)區(qū)”都會以某種形式存在于每一個Java虛擬機實現(xiàn)中,但是規(guī)范對它們的描述卻是相當抽象的。這些運行時數(shù)據(jù)區(qū)結構上的細節(jié),大多數(shù)都由具體實現(xiàn)的設計者決定。
不同的虛擬機實現(xiàn)可能具有很不同的內存限制,有的實現(xiàn)可能大量的內存可用,有的可能只有很少的內存,有的實現(xiàn)可以利用虛擬內存,有的則不能。規(guī)范本身對“運行時數(shù)據(jù)區(qū)”只有抽象的描述,這就使得Java虛擬機可以很容易地在各種計算機和設備上實現(xiàn)。
某些運行時數(shù)據(jù)區(qū)是由程序匯總所有線程共享的,還有一些則只由一個線程擁有。每個Java虛擬機實例都有一個方法區(qū)以及一個堆,它們是由該虛擬機實例中所有線程共享的。當虛擬機裝載一個class文件時,它會從這個class文件包含的二進制數(shù)據(jù)中解析類型信息,然后,它把這些類型信息放到方法區(qū)中。當程序運行時,虛擬機會把所有該程序在運行時創(chuàng)建的對象都放到堆中。圖2-4對這些內存區(qū)域進行了描繪。

圖2-4 由所有線程共享的運行時數(shù)據(jù)區(qū)
當每一個新線程被創(chuàng)建時,它都將得到它自己的PC寄存器(程序計數(shù)器)以及一個Java棧:如果線程正在執(zhí)行的是一個Java方法(非本地方法),那么PC寄存器的值將總是指示下一條將被執(zhí)行的指令,而它的Java棧則總是存儲該線程中Java方法調用的狀態(tài)——包括它的局部變量、被調用時傳進來的參數(shù)、它的返回值以及運算的中間結果等。而本地方法調用的狀態(tài),則是以某種依賴于具體實現(xiàn)的方式存儲在本地方法棧中,也可能是在寄存器或者其他某些與特定實現(xiàn)相關的內存區(qū)中。
Java棧是由許多棧幀(stackframe)或者說幀(frame)組成的,一個棧幀包含一個Java方法調用的狀態(tài)。當線程調用一個Java方法時,虛擬機壓入一個新的棧幀到該線程的Java棧中。當該方法返回時,這個棧幀被從Java棧中彈出并拋棄。
Java虛擬機沒有寄存器,其指令集使用Java棧來存儲中間數(shù)據(jù)。這樣設計的原因是為了保持Java虛擬機的指令集盡量緊湊,同時也便于Java虛擬機在那些只有很少通用寄存器的平臺上實現(xiàn),另外Java虛擬機的這種基于棧的體系結構,也有助于運行時某些虛擬機實現(xiàn)的動態(tài)編譯器和即時編譯器的代碼優(yōu)化。
圖2-5描繪了Java虛擬機為每一個線程創(chuàng)建的內存區(qū),這些內存區(qū)域是私有的,任何線程都不能訪問另一個線程的PC寄存器或者Java棧。

圖2-5 線程專有的運行時數(shù)據(jù)區(qū)
圖2-5展示了一個虛擬機實例的快照,它有3個線程正在執(zhí)行。線程1和線程2都正在執(zhí)行Java方法,而線程3則正在執(zhí)行一個本地方法。在圖5-3中,和本書其他地方一樣,Java棧都是向下生長的,而棧頂都顯示在圖的底部,當前正在執(zhí)行的方法的棧幀則以淺色表示,對于一個正在運行Java方法的線程而言,它的PC寄存器總是指向下一條將被執(zhí)行的指令。在圖2-5中,像這樣的PC寄存器(比如線程1和線程2的)都是以淺色顯示的。由于線程3當前正在執(zhí)行一個本地方法,因此,它的PC寄存器(以深色顯示的那個)的值是不確定的。
2.2.3 JVM的生命周期
一個運行時的Java虛擬機實例的天職是:負責運行一個Java程序。在啟動一個Java程序的同時會誕生一個虛擬機實例,當該程序退出時,虛擬機實例也隨之消亡。如果在同一臺計算機上同時運行3個Java程序,會得到3個Java虛擬機實例。每個Java程序都運行于它自己的Java虛擬機實例中。
Java虛擬機實例通過調用某個初始類的main()方法來運行一個Java程序。而這個main()方法必須是公有的(public)、靜態(tài)的(static)、返回值為void,并且接受一個字符串數(shù)組作為參數(shù)。任何擁有這樣一個main()方法的類都可以作為Java程序運行的起點。假如存在這樣一個Java程序,此程序能夠打印出傳給它的命令行參數(shù):
package jvm.ext1; public class Echo { public static void main(String[]args) { int length = args.length; for (int i = 0; i <length; i++) { System.out.print(args[i] +""); } System.out.println(); } }
上述代碼必須告訴Java虛擬機要運行的Java程序中初始類的名字,整個程序將從它的main()方法開始運行。現(xiàn)實中一個Java虛擬機實現(xiàn)的例子如SunJava 2 SDK的Java程序。比如,如果想要在Windows上使用Java運行Echo程序,需要鍵入如下命令。
java Echo Greeting, Planet
該命令的第一個單詞“java”,告訴操作系統(tǒng)應該運行來自Sun Java 2 SDK的Java虛擬機。第二個詞”Echo”則支持初始類的名字。Echo這個初始類中必須有個公有的、靜態(tài)的方法main(),它獲得一個字符串數(shù)組參數(shù)并且返回void。上述命令行中剩下的單詞序列“Greeting, Planet”,作為該程序的命令行參數(shù)以字符串數(shù)組的形式傳遞給main(),因此,對于上面這個例子,傳遞給類Echo中main()方法的字符串數(shù)組參數(shù)的內容就是:
args[0]為”Greeting, ” args[1]為“Planet.”
Java程序初始類中的main()方法,將作為該程序初始線程的起點,任何其他的線程都是由這個初始線程啟動的。
在Java虛擬機內部有兩種線程:守護線程與非守護線程。守護線程通常是由虛擬機自己使用的,比如執(zhí)行垃圾收集任務的線程。但是,Java程序也可以把它創(chuàng)建的任何線程標記為守護線程。而Java程序中的初始線程(即程序開始的main())是非守護線程。
只要還有任何非守護線程在運行,那么這個Java程序也在繼續(xù)運行(虛擬機仍然存活)。當該程序中所有的非守護線程都終止時,虛擬機實例將自動退出。假若安全管理器允許,程序本身也能夠通過調用Runtime類或者System類的exit方法來退出。
在上面的Echo程序中,方法main()并沒有調用其他的線程。所以當它打印完命令行參數(shù)后返回main()方法。這就終止了該程序中唯一的非守護線程,最終導致虛擬機實例退出。
- Creo Parametric 8.0中文版基礎入門一本通
- 性能測試從零開始
- Adobe 創(chuàng)意大學動漫設計師Flash CS5 + Photoshop CS5 標準實訓教材
- Photoshop 平面廣告設計從入門到精通
- CorelDRAW服裝設計實用教程(第四版)
- MLOps實戰(zhàn):機器學習模型的開發(fā)、部署與應用
- SolidWorks 2018有限元:運動仿真與流場分析自學手冊
- VRP11/3ds Max虛擬現(xiàn)實制作標準實訓教程
- Excel革命!超級數(shù)據(jù)透視表Power Pivot與數(shù)據(jù)分析表達式DAX快速入門
- 從零開始:Indesign CC 2019設計基礎+商業(yè)設計實戰(zhàn)
- 邊做邊學:CorelDRAW X6圖形設計案例教程(第2版)(微課版)
- Microsoft Windows Communication Foundation 4.0 Cookbook for Developing SOA Applications
- Moodle 1.9 for Design and Technology
- 中文版Corel DRAW X5案例實訓教材
- Photoshop CS6 中文版從入門到精通