posts - 120,  comments - 19,  trackbacks - 0

          Java虛擬機(jī)的深入研究

          作者:劉學(xué)超

          1??Java技術(shù)與Java虛擬機(jī)

          說起Java,人們首先想到的是Java編程語言,然而事實(shí)上,Java是一種技術(shù),它由四方面組成: Java編程語言、Java類文件格式、Java虛擬機(jī)和Java應(yīng)用程序接口(Java API)。它們的關(guān)系如下圖所示:

          圖1??Java四個方面的關(guān)系

          運(yùn)行期環(huán)境代表著Java平臺,開發(fā)人員編寫Java代碼(.java文件),然后將之編譯成字節(jié)碼(.class文件)。最后字節(jié)碼被裝入內(nèi)存,一旦字節(jié)碼進(jìn)入虛擬機(jī),它就會被解釋器解釋執(zhí)行,或者是被即時代碼發(fā)生器有選擇的轉(zhuǎn)換成機(jī)器碼執(zhí)行。從上圖也可以看出Java平臺由Java虛擬機(jī)和Java應(yīng)用程序接口搭建,Java語言則是進(jìn)入這個平臺的通道,用Java語言編寫并編譯的程序可以運(yùn)行在這個平臺上。這個平臺的結(jié)構(gòu)如下圖所示:

          在Java平臺的結(jié)構(gòu)中, 可以看出,Java虛擬機(jī)(JVM) 處在核心的位置,是程序與底層操作系統(tǒng)和硬件無關(guān)的關(guān)鍵。它的下方是移植接口,移植接口由兩部分組成:適配器和Java操作系統(tǒng), 其中依賴于平臺的部分稱為適配器;JVM 通過移植接口在具體的平臺和操作系統(tǒng)上實(shí)現(xiàn);在JVM 的上方是Java的基本類庫和擴(kuò)展類庫以及它們的API, 利用Java API編寫的應(yīng)用程序(application) 和小程序(Java applet) 可以在任何Java平臺上運(yùn)行而無需考慮底層平臺, 就是因?yàn)橛蠮ava虛擬機(jī)(JVM)實(shí)現(xiàn)了程序與操作系統(tǒng)的分離,從而實(shí)現(xiàn)了Java 的平臺無關(guān)性。

          那么到底什么是Java虛擬機(jī)(JVM)呢?通常我們談?wù)揓VM時,我們的意思可能是:

          1. 對JVM規(guī)范的的比較抽象的說明;
          2. 對JVM的具體實(shí)現(xiàn);
          3. 在程序運(yùn)行期間所生成的一個JVM實(shí)例。

          對JVM規(guī)范的的抽象說明是一些概念的集合,它們已經(jīng)在書《The Java Virtual Machine Specification》(《Java虛擬機(jī)規(guī)范》)中被詳細(xì)地描述了;對JVM的具體實(shí)現(xiàn)要么是軟件,要么是軟件和硬件的組合,它已經(jīng)被許多生產(chǎn)廠商所實(shí)現(xiàn),并存在于多種平臺之上;運(yùn)行Java程序的任務(wù)由JVM的運(yùn)行期實(shí)例單個承擔(dān)。在本文中我們所討論的Java虛擬機(jī)(JVM)主要針對第三種情況而言。它可以被看成一個想象中的機(jī)器,在實(shí)際的計(jì)算機(jī)上通過軟件模擬來實(shí)現(xiàn),有自己想象中的硬件,如處理器、堆棧、寄存器等,還有自己相應(yīng)的指令系統(tǒng)。

          JVM在它的生存周期中有一個明確的任務(wù),那就是運(yùn)行Java程序,因此當(dāng)Java程序啟動的時候,就產(chǎn)生JVM的一個實(shí)例;當(dāng)程序運(yùn)行結(jié)束的時候,該實(shí)例也跟著消失了。下面我們從JVM的體系結(jié)構(gòu)和它的運(yùn)行過程這兩個方面來對它進(jìn)行比較深入的研究。

          2??Java虛擬機(jī)的體系結(jié)構(gòu)

          剛才已經(jīng)提到,JVM可以由不同的廠商來實(shí)現(xiàn)。由于廠商的不同必然導(dǎo)致JVM在實(shí)現(xiàn)上的一些不同,然而JVM還是可以實(shí)現(xiàn)跨平臺的特性,這就要?dú)w功于設(shè)計(jì)JVM時的體系結(jié)構(gòu)了。

          我們知道,一個JVM實(shí)例的行為不光是它自己的事,還涉及到它的子系統(tǒng)、存儲區(qū)域、數(shù)據(jù)類型和指令這些部分,它們描述了JVM的一個抽象的內(nèi)部體系結(jié)構(gòu),其目的不光規(guī)定實(shí)現(xiàn)JVM時它內(nèi)部的體系結(jié)構(gòu),更重要的是提供了一種方式,用于嚴(yán)格定義實(shí)現(xiàn)時的外部行為。每個JVM都有兩種機(jī)制,一個是裝載具有合適名稱的類(類或是接口),叫做類裝載子系統(tǒng);另外的一個負(fù)責(zé)執(zhí)行包含在已裝載的類或接口中的指令,叫做運(yùn)行引擎。每個JVM又包括方法區(qū)、堆、Java棧、程序計(jì)數(shù)器和本地方法棧這五個部分,這幾個部分和類裝載機(jī)制與運(yùn)行引擎機(jī)制一起組成的體系結(jié)構(gòu)圖為:

          圖3??JVM的體系結(jié)構(gòu)

          JVM的每個實(shí)例都有一個它自己的方法域和一個堆,運(yùn)行于JVM內(nèi)的所有的線程都共享這些區(qū)域;當(dāng)虛擬機(jī)裝載類文件的時候,它解析其中的二進(jìn)制數(shù)據(jù)所包含的類信息,并把它們放到方法域中;當(dāng)程序運(yùn)行的時候,JVM把程序初始化的所有對象置于堆上;而每個線程創(chuàng)建的時候,都會擁有自己的程序計(jì)數(shù)器和Java棧,其中程序計(jì)數(shù)器中的值指向下一條即將被執(zhí)行的指令,線程的Java棧則存儲為該線程調(diào)用Java方法的狀態(tài);本地方法調(diào)用的狀態(tài)被存儲在本地方法棧,該方法棧依賴于具體的實(shí)現(xiàn)。

          下面分別對這幾個部分進(jìn)行說明。

          執(zhí)行引擎處于JVM的核心位置,在Java虛擬機(jī)規(guī)范中,它的行為是由指令集所決定的。盡管對于每條指令,規(guī)范很詳細(xì)地說明了當(dāng)JVM執(zhí)行字節(jié)碼遇到指令時,它的實(shí)現(xiàn)應(yīng)該做什么,但對于怎么做卻言之甚少。Java虛擬機(jī)支持大約248個字節(jié)碼。每個字節(jié)碼執(zhí)行一種基本的CPU運(yùn)算,例如,把一個整數(shù)加到寄存器,子程序轉(zhuǎn)移等。Java指令集相當(dāng)于Java程序的匯編語言。

          Java指令集中的指令包含一個單字節(jié)的操作符,用于指定要執(zhí)行的操作,還有0個或多個操作數(shù),提供操作所需的參數(shù)或數(shù)據(jù)。許多指令沒有操作數(shù),僅由一個單字節(jié)的操作符構(gòu)成。

          虛擬機(jī)的內(nèi)層循環(huán)的執(zhí)行過程如下: 
          do{ 
          取一個操作符字節(jié); 
          根據(jù)操作符的值執(zhí)行一個動作; 
          }while(程序未結(jié)束)
          

          由于指令系統(tǒng)的簡單性,使得虛擬機(jī)執(zhí)行的過程十分簡單,從而有利于提高執(zhí)行的效率。指令中操作數(shù)的數(shù)量和大小是由操作符決定的。如果操作數(shù)比一個字節(jié)大,那么它存儲的順序是高位字節(jié)優(yōu)先。例如,一個16位的參數(shù)存放時占用兩個字節(jié),其值為:

          第一個字節(jié)*256+第二個字節(jié)字節(jié)碼。

          指令流一般只是字節(jié)對齊的。指令tableswitch和lookup是例外,在這兩條指令內(nèi)部要求強(qiáng)制的4字節(jié)邊界對齊。

          對于本地方法接口,實(shí)現(xiàn)JVM并不要求一定要有它的支持,甚至可以完全沒有。Sun公司實(shí)現(xiàn)Java本地接口(JNI)是出于可移植性的考慮,當(dāng)然我們也可以設(shè)計(jì)出其它的本地接口來代替Sun公司的JNI。但是這些設(shè)計(jì)與實(shí)現(xiàn)是比較復(fù)雜的事情,需要確保垃圾回收器不會將那些正在被本地方法調(diào)用的對象釋放掉。

          Java的堆是一個運(yùn)行時數(shù)據(jù)區(qū),類的實(shí)例(對象)從中分配空間,它的管理是由垃圾回收來負(fù)責(zé)的:不給程序員顯式釋放對象的能力。Java不規(guī)定具體使用的垃圾回收算法,可以根據(jù)系統(tǒng)的需求使用各種各樣的算法。

          Java方法區(qū)與傳統(tǒng)語言中的編譯后代碼或是Unix進(jìn)程中的正文段類似。它保存方法代碼(編譯后的java代碼)和符號表。在當(dāng)前的Java實(shí)現(xiàn)中,方法代碼不包括在垃圾回收堆中,但計(jì)劃在將來的版本中實(shí)現(xiàn)。每個類文件包含了一個Java類或一個Java界面的編譯后的代碼。可以說類文件是Java語言的執(zhí)行代碼文件。為了保證類文件的平臺無關(guān)性,Java虛擬機(jī)規(guī)范中對類文件的格式也作了詳細(xì)的說明。其具體細(xì)節(jié)請參考Sun公司的Java虛擬機(jī)規(guī)范。

          Java虛擬機(jī)的寄存器用于保存機(jī)器的運(yùn)行狀態(tài),與微處理器中的某些專用寄存器類似。Java虛擬機(jī)的寄存器有四種:

          1. pc: Java程序計(jì)數(shù)器;
          2. optop: 指向操作數(shù)棧頂端的指針;
          3. frame: 指向當(dāng)前執(zhí)行方法的執(zhí)行環(huán)境的指針;。
          4. vars: 指向當(dāng)前執(zhí)行方法的局部變量區(qū)第一個變量的指針。

          在上述體系結(jié)構(gòu)圖中,我們所說的是第一種,即程序計(jì)數(shù)器,每個線程一旦被創(chuàng)建就擁有了自己的程序計(jì)數(shù)器。當(dāng)線程執(zhí)行Java方法的時候,它包含該線程正在被執(zhí)行的指令的地址。但是若線程執(zhí)行的是一個本地的方法,那么程序計(jì)數(shù)器的值就不會被定義。

          Java虛擬機(jī)的棧有三個區(qū)域:局部變量區(qū)、運(yùn)行環(huán)境區(qū)、操作數(shù)區(qū)。

          局部變量區(qū)

          每個Java方法使用一個固定大小的局部變量集。它們按照與vars寄存器的字偏移量來尋址。局部變量都是32位的。長整數(shù)和雙精度浮點(diǎn)數(shù)占據(jù)了兩個局部變量的空間,卻按照第一個局部變量的索引來尋址。(例如,一個具有索引n的局部變量,如果是一個雙精度浮點(diǎn)數(shù),那么它實(shí)際占據(jù)了索引n和n+1所代表的存儲空間)虛擬機(jī)規(guī)范并不要求在局部變量中的64位的值是64位對齊的。虛擬機(jī)提供了把局部變量中的值裝載到操作數(shù)棧的指令,也提供了把操作數(shù)棧中的值寫入局部變量的指令。

          運(yùn)行環(huán)境區(qū)

          在運(yùn)行環(huán)境中包含的信息用于動態(tài)鏈接,正常的方法返回以及異常捕捉。

          動態(tài)鏈接

          運(yùn)行環(huán)境包括對指向當(dāng)前類和當(dāng)前方法的解釋器符號表的指針,用于支持方法代碼的動態(tài)鏈接。方法的class文件代碼在引用要調(diào)用的方法和要訪問的變量時使用符號。動態(tài)鏈接把符號形式的方法調(diào)用翻譯成實(shí)際方法調(diào)用,裝載必要的類以解釋還沒有定義的符號,并把變量訪問翻譯成與這些變量運(yùn)行時的存儲結(jié)構(gòu)相應(yīng)的偏移地址。動態(tài)鏈接方法和變量使得方法中使用的其它類的變化不會影響到本程序的代碼。

          正常的方法返回

          如果當(dāng)前方法正常地結(jié)束了,在執(zhí)行了一條具有正確類型的返回指令時,調(diào)用的方法會得到一個返回值。執(zhí)行環(huán)境在正常返回的情況下用于恢復(fù)調(diào)用者的寄存器,并把調(diào)用者的程序計(jì)數(shù)器增加一個恰當(dāng)?shù)臄?shù)值,以跳過已執(zhí)行過的方法調(diào)用指令,然后在調(diào)用者的執(zhí)行環(huán)境中繼續(xù)執(zhí)行下去。

          異常捕捉

          異常情況在Java中被稱作Error(錯誤)或Exception(異常),是Throwable類的子類,在程序中的原因是:①動態(tài)鏈接錯,如無法找到所需的class文件。②運(yùn)行時錯,如對一個空指針的引用。程序使用了throw語句。

          當(dāng)異常發(fā)生時,Java虛擬機(jī)采取如下措施:

          • 檢查與當(dāng)前方法相聯(lián)系的catch子句表。每個catch子句包含其有效指令范圍,能夠處理的異常類型,以及處理異常的代碼塊地址。
          • 與異常相匹配的catch子句應(yīng)該符合下面的條件:造成異常的指令在其指令范圍之內(nèi),發(fā)生的異常類型是其能處理的異常類型的子類型。如果找到了匹配的catch子句,那么系統(tǒng)轉(zhuǎn)移到指定的異常處理塊處執(zhí)行;如果沒有找到異常處理塊,重復(fù)尋找匹配的catch子句的過程,直到當(dāng)前方法的所有嵌套的catch子句都被檢查過。
          • 由于虛擬機(jī)從第一個匹配的catch子句處繼續(xù)執(zhí)行,所以catch子句表中的順序是很重要的。因?yàn)镴ava代碼是結(jié)構(gòu)化的,因此總可以把某個方法的所有的異常處理器都按序排列到一個表中,對任意可能的程序計(jì)數(shù)器的值,都可以用線性的順序找到合適的異常處理塊,以處理在該程序計(jì)數(shù)器值下發(fā)生的異常情況。
          • 如果找不到匹配的catch子句,那么當(dāng)前方法得到一個"未截獲異常"的結(jié)果并返回到當(dāng)前方法的調(diào)用者,好像異常剛剛在其調(diào)用者中發(fā)生一樣。如果在調(diào)用者中仍然沒有找到相應(yīng)的異常處理塊,那么這種錯誤將被傳播下去。如果錯誤被傳播到最頂層,那么系統(tǒng)將調(diào)用一個缺省的異常處理塊。

          操作數(shù)棧區(qū)

          機(jī)器指令只從操作數(shù)棧中取操作數(shù),對它們進(jìn)行操作,并把結(jié)果返回到棧中。選擇棧結(jié)構(gòu)的原因是:在只有少量寄存器或非通用寄存器的機(jī)器(如Intel486)上,也能夠高效地模擬虛擬機(jī)的行為。操作數(shù)棧是32位的。它用于給方法傳遞參數(shù),并從方法接收結(jié)果,也用于支持操作的參數(shù),并保存操作的結(jié)果。例如,iadd指令將兩個整數(shù)相加。相加的兩個整數(shù)應(yīng)該是操作數(shù)棧頂?shù)膬蓚€字。這兩個字是由先前的指令壓進(jìn)堆棧的。這兩個整數(shù)將從堆棧彈出、相加,并把結(jié)果壓回到操作數(shù)棧中。

          每個原始數(shù)據(jù)類型都有專門的指令對它們進(jìn)行必須的操作。每個操作數(shù)在棧中需要一個存儲位置,除了long和double型,它們需要兩個位置。操作數(shù)只能被適用于其類型的操作符所操作。例如,壓入兩個int類型的數(shù),如果把它們當(dāng)作是一個long類型的數(shù)則是非法的。在Sun的虛擬機(jī)實(shí)現(xiàn)中,這個限制由字節(jié)碼驗(yàn)證器強(qiáng)制實(shí)行。但是,有少數(shù)操作(操作符dupe和swap),用于對運(yùn)行時數(shù)據(jù)區(qū)進(jìn)行操作時是不考慮類型的。

          本地方法棧,當(dāng)一個線程調(diào)用本地方法時,它就不再受到虛擬機(jī)關(guān)于結(jié)構(gòu)和安全限制方面的約束,它既可以訪問虛擬機(jī)的運(yùn)行期數(shù)據(jù)區(qū),也可以使用本地處理器以及任何類型的棧。例如,本地棧是一個C語言的棧,那么當(dāng)C程序調(diào)用C函數(shù)時,函數(shù)的參數(shù)以某種順序被壓入棧,結(jié)果則返回給調(diào)用函數(shù)。在實(shí)現(xiàn)Java虛擬機(jī)時,本地方法接口使用的是C語言的模型棧,那么它的本地方法棧的調(diào)度與使用則完全與C語言的棧相同。

          3??Java虛擬機(jī)的運(yùn)行過程

          上面對虛擬機(jī)的各個部分進(jìn)行了比較詳細(xì)的說明,下面通過一個具體的例子來分析它的運(yùn)行過程。

          虛擬機(jī)通過調(diào)用某個指定類的方法main啟動,傳遞給main一個字符串?dāng)?shù)組參數(shù),使指定的類被裝載,同時鏈接該類所使用的其它的類型,并且初始化它們。例如對于程序:

          class HelloApp 
          {
          	public static void main(String[] args) 
          	{
          		System.out.println("Hello World!"); 
          		for (int i = 0; i < args.length; i++ )
          		{
          			System.out.println(args[i]);
          		}
          	}
          }
          

          編譯后在命令行模式下鍵入: java HelloApp run virtual machine

          將通過調(diào)用HelloApp的方法main來啟動java虛擬機(jī),傳遞給main一個包含三個字符串"run"、"virtual"、"machine"的數(shù)組。現(xiàn)在我們略述虛擬機(jī)在執(zhí)行HelloApp時可能采取的步驟。

          開始試圖執(zhí)行類HelloApp的main方法,發(fā)現(xiàn)該類并沒有被裝載,也就是說虛擬機(jī)當(dāng)前不包含該類的二進(jìn)制代表,于是虛擬機(jī)使用ClassLoader試圖尋找這樣的二進(jìn)制代表。如果這個進(jìn)程失敗,則拋出一個異常。類被裝載后同時在main方法被調(diào)用之前,必須對類HelloApp與其它類型進(jìn)行鏈接然后初始化。鏈接包含三個階段:檢驗(yàn),準(zhǔn)備和解析。檢驗(yàn)檢查被裝載的主類的符號和語義,準(zhǔn)備則創(chuàng)建類或接口的靜態(tài)域以及把這些域初始化為標(biāo)準(zhǔn)的默認(rèn)值,解析負(fù)責(zé)檢查主類對其它類或接口的符號引用,在這一步它是可選的。類的初始化是對類中聲明的靜態(tài)初始化函數(shù)和靜態(tài)域的初始化構(gòu)造方法的執(zhí)行。一個類在初始化之前它的父類必須被初始化。整個過程如下:

          圖4:虛擬機(jī)的運(yùn)行過程

          4??結(jié)束語

          本文通過對JVM的體系結(jié)構(gòu)的深入研究以及一個Java程序執(zhí)行時虛擬機(jī)的運(yùn)行過程的詳細(xì)分析,意在剖析清楚Java虛擬機(jī)的機(jī)理。



          posted on 2006-10-15 15:39 阿成 閱讀(369) 評論(0)  編輯  收藏 所屬分類: Java
          主站蜘蛛池模板: 资溪县| 美姑县| 绥滨县| 确山县| 新营市| 光泽县| 广汉市| 开远市| 阳城县| 辽阳市| 碌曲县| 包头市| 灌南县| 磐安县| 绥宁县| 南岸区| 沁阳市| 肥西县| 沧州市| 德阳市| 绍兴市| 湘西| 九龙坡区| 淮北市| 时尚| 左云县| 个旧市| 德清县| 盐边县| 哈尔滨市| 囊谦县| 来安县| 中方县| 历史| 新巴尔虎右旗| 福鼎市| 福安市| 长垣县| 武宣县| 咸丰县| 平阳县|