眾所周知,Java源代碼被編譯器編譯成class文件。而并不是底層操作系統(tǒng)可以直接執(zhí)行的二進(jìn)制指令(比如Windows OS的.exe文件)。因此,我們需要有一種平臺可以解釋class文件并運(yùn)行它。而做到這一點(diǎn)的正是Java 虛擬機(jī)(JVM)。
實(shí)際上,JVM是一種解釋執(zhí)行class文件的規(guī)范技術(shù)。各 個提 供商都可以根據(jù)規(guī)范,在不同的底層平臺上實(shí)現(xiàn)不同的JVM。
下面是JVM實(shí)現(xiàn)的基本結(jié)構(gòu)框圖。其中類裝載子系統(tǒng)、運(yùn)行時數(shù)據(jù)區(qū)、執(zhí)行引擎等 是JVM的必須要解決的幾大問題。
![]() ★ 類裝載器子系統(tǒng) 字段名 字段的類型 字段的修飾符(public, private, protected, static, final, volatile, transient的某個子集) ●方法信息。和字段一樣保存方法的相關(guān)信息。 方法名 方法的返回類型 方法的參數(shù)的數(shù)量和類型 方法的修飾符 方法的字節(jié)碼 操作數(shù)棧和棧幀中局部變量的大小 (見下面Java棧的內(nèi)容) 異常表
(2) 堆 Java 程序在運(yùn)行時創(chuàng)建的所有類型對象和數(shù)組都存儲在堆中。JVM會根據(jù)new指令在堆中開辟一個確定類型的對象內(nèi)存空間。但是堆中開辟對象的空間并沒有任何 人工 指令可以回收,而是通過JVM的垃圾回收器負(fù)責(zé)回收。
堆中對象存儲的是該對象以及對象所有超類的實(shí)例數(shù)據(jù)(但不是靜態(tài)數(shù)據(jù)), 比如下面的類型: class X{ private int data; private static int stcdata=0;
public X(int d){ this.data=d; } } X x1=new X(100); X x2=new X(200); 這樣在堆中開辟了兩個對象x1和x2的內(nèi)存空間。其中x1中的一個實(shí)例數(shù)據(jù)data=100,而x2的data=200。但是這兩個對象中都沒有stcdata這樣的數(shù)據(jù),這個靜態(tài)數(shù)據(jù)存儲在上面講到的方法區(qū)中。
此外,堆中對象還必須有指向方法區(qū)中的類信息數(shù)據(jù)(見上面方法區(qū))。 為什么需要這個信息呢?因?yàn)楫?dāng)程序在運(yùn)行時需要對象轉(zhuǎn)型,那么JVM必須檢查當(dāng)前對象所屬類型及父類的信息。以判斷轉(zhuǎn)型是否是合法的,而這一點(diǎn)也是instanceof操作符實(shí)現(xiàn)的基礎(chǔ)。
當(dāng)然,上述只是JVM的規(guī)范,具體堆的實(shí)現(xiàn)是由JVM設(shè)計(jì)者來決定。下面兩幅圖就直觀的表現(xiàn)出了堆對象的不同實(shí)現(xiàn)結(jié)構(gòu): 其中一個對象的引用可能在整個運(yùn)行時數(shù)據(jù)區(qū)中的很多地方存在,比如Java棧,堆,方法區(qū)等。
堆中對象還應(yīng)該關(guān)聯(lián)一個對象的鎖數(shù)據(jù)信息以及線程的等待集合。 這些都是實(shí)現(xiàn)Java線程同步機(jī)制的基礎(chǔ)。但實(shí)際上很多具體實(shí)現(xiàn)中并不在對象自身內(nèi)部保存一個指向鎖數(shù)據(jù)的指針。而只有當(dāng)?shù)谝淮涡枰渔i的時候才分配對應(yīng)鎖數(shù)據(jù)。另外,每個對象都會從Object中繼承三個Object方法(wait、notify、notifyAll),當(dāng)某個線程在一個對象上調(diào)用了等待方法時。JVM就會阻塞這個線程,并把這個線程放在該對象的等待集合中。知道另外一個線程在該對象上調(diào)用了notify/notifyAll,JVM才會在等待集合中喚醒一個或全部的等待線程(參見《正確理解線程等待和釋放(wait/notify)》)。
【數(shù)組對象】 在Java中,數(shù)組也是對象,那么自然在堆中會存儲數(shù)組的信息。事實(shí)也確實(shí)如此,對于JVM而言,數(shù)組與其他類對象沒有任何區(qū)別。 數(shù)組也有屬于的類Class,具有相同維度和類型的數(shù)組都是同一個類的實(shí)例,而不管數(shù)組的長度是多少。 數(shù)組類的名稱由兩部分構(gòu)成:(1)每一維用一個方括號“[”表示。(2) 用字符或字符串表示元素類型。比如一維數(shù)組對象int[] a所屬類型名為"[I",二維數(shù)組對象byte[] b所屬類型名為"[[B"。 下圖是二維數(shù)組對象在堆中的具體實(shí)現(xiàn)方式:
(3) 程序計(jì)數(shù)器
對于一個運(yùn)行的Java而言,每一個線程都有一個PC寄存器。當(dāng)線程執(zhí)行Java程序時,PC寄存器的內(nèi)容總是下一條將被執(zhí)行的指令地址。
(4) Java棧 - 棧幀
每啟動一個線程,JVM都會為它分配一個Java棧,用于存放方法中的局部變量,操作數(shù)以及異常數(shù)據(jù)等。當(dāng)線程調(diào)用某個方法時,JVM會根據(jù)方法區(qū)中該方法的字節(jié)碼組建一個棧幀。并將該棧幀壓入Java棧中,方法執(zhí)行完畢時,JVM會彈出該棧幀并釋放掉。 注意,Java棧中的數(shù)據(jù)是線程私有的,一個線程是無法訪問另一個線程的Java棧的數(shù)據(jù)。這也就是為什么多線程編程時,兩個相同線程執(zhí)行同一方法時,對方法內(nèi)的局部變量時不需要數(shù)據(jù)同步的原因。
【棧幀 】 棧幀有三部分構(gòu)成:局部變量區(qū)、操作數(shù)棧和幀數(shù)據(jù)區(qū)。在編譯器編譯Java代碼時,就已經(jīng)在字節(jié)碼中為每個方法都設(shè)置好了局部變量區(qū)和操作數(shù)棧的數(shù)據(jù)和大小。并在JVM首次加載方法所屬的Class文件時,就將這些數(shù)據(jù)放進(jìn)了方法區(qū)。因此在線程調(diào)用方法時,只需要根據(jù)方法區(qū)中的局部變量區(qū)和操作數(shù)棧的大小來分配一個新的棧幀的內(nèi)存大小,并堆入Java棧。
局部變量區(qū): 用來存放方法中的所有局部變量值,包括傳遞的參數(shù)。這些數(shù)據(jù)會被組織成以一個字長(32bit或64bit)為單位的數(shù)組結(jié)構(gòu)(以索引0開始)中。其中類型為int, float, reference(引用類型,記錄對象在堆中地址)和returnAddress(一種JVM內(nèi)部使用的基本類型)的值占用1個字長,而byte, char和shot會擴(kuò)大成1個字長存儲,long,double則使用2個字長。
操作數(shù)棧: 用來在執(zhí)行指令的時候存儲和使用中間結(jié)果數(shù)據(jù)。
幀數(shù)據(jù)區(qū): 常量池的解析,正常方法返回以及異常派發(fā)機(jī)制的信息數(shù)據(jù)都存儲在其中。
下圖展示了addAndPrint()調(diào)用addTwoTypes()時,Java棧的變化:
★ 執(zhí)行引擎 運(yùn)行Java的每一個線程都是一個獨(dú)立的虛擬機(jī)執(zhí)行引擎的實(shí)例。從線程生命周期的開始到結(jié)束,他要么在執(zhí)行字節(jié)碼,要么在執(zhí)行本地方法。一個線程可能通過解釋或者使用芯片級指令直接執(zhí)行字節(jié)碼,或者間接通過JIT執(zhí)行編譯過的本地代碼。
指令集: 實(shí)際上,Class文件中方法的字節(jié)碼流就是有JVM的指令序列構(gòu)成的。每一條指令包含一個單字節(jié)的操作碼,后面跟隨0個或多個操作數(shù)。
Java虛擬機(jī)指令集關(guān)注的中心是操作數(shù)棧和局部變量集合。我們可以看看下面一組指令在執(zhí)行引擎中執(zhí)行的過程:
很顯然,上面的指令反復(fù)用到了Java棧中的某一個方法棧幀。實(shí)際上執(zhí)行引擎運(yùn)行Java字節(jié)碼指令很多時候都是在不停的操作Java棧,也有的時候需要在堆中開辟對象以及運(yùn)行系統(tǒng)的本地指令等。但是Java棧的操作要比堆中的操作要快的多,因此反復(fù)開辟對象是非常耗時的。這也是為什么Java程序優(yōu)化的時候,盡量減少new對象。
下面將會是很有趣的過程,我們用一段代碼來生動的展現(xiàn)JVM是如何運(yùn)行這段程序的。
通過編譯器將下面的代碼編譯成edu/hr/jvm/Test.class 和 edu/hr/jvm/bean/Act.class。然后開始啟動JVM:
(5) JVM加載進(jìn)Act.class,并提取Act類信息 放入方法區(qū)中。見上圖方法區(qū)所示,然后以一個直接指向方法區(qū)Act類信息的直接引用替換開始在常量池中的符號引用"Act",這個過程就是常量池解析 。以后就可以直接訪問Act的類信息了。
(6) 此時JVM可以根據(jù)方法區(qū)中的Act類信息,在堆中開辟一個Act類對象 act。見上圖堆所示。
(7) 接著開始執(zhí)行main方法中的第二條指令調(diào)用doMathForever。這個可以通過堆中act對象所指的方法表 中查找,然后定位到方法區(qū)中的Act類信息中的doMathForever方法字節(jié)碼。在運(yùn)行之前,仍然要組建一個doMathForever棧幀壓入Java棧,如上圖所示。(注意:JVM會根據(jù)方法區(qū)中doMathForever的字節(jié)碼來創(chuàng)建棧幀的局部變量區(qū)和操作數(shù)棧的大小)
● 下面運(yùn)行每一條指令后,看一下局部變量區(qū)和操作數(shù)棧的變化: ① 指令[iconst_0] 將int類型變量的數(shù)據(jù)0壓入操作數(shù)棧。 局部變量區(qū) 操作數(shù)棧 index hex value value offset hex value value (變量i) 0 0 00000000 0 optop-> 1 ② 指令[istore_0] 彈出操作數(shù)棧頂?shù)臄?shù)據(jù)0,將結(jié)果存儲在局部變量區(qū)中index=0的空間中。 局部變量區(qū) 操作數(shù)棧 index hex value value offset hex value value (變量i) 0 00000000 0 optop-> 0 1 ③指令[iinc 0 1] 把常量值1加到局部變量區(qū)中index=0的空間上。 局部變量區(qū) 操作數(shù)棧 index hex value value offset hex value value (變量i) 0 00000001 1 optop-> 0 1 ④指令[iload_0] 把局部變量區(qū)index=0中的數(shù)據(jù)堆入操作數(shù)棧。 局部變量區(qū) 操作數(shù)棧 index hex value value offset hex value value (變量i) 0 00000001 1 0 00000001 1 optop-> 1 ⑤指令[iconst_2] 把int類型變量的數(shù)據(jù)2壓入操作數(shù)棧。 局部變量區(qū) 操作數(shù)棧 index hex value value offset hex value value (變量i) 0 00000001 1 0 00000001 1 1 00000002 2 optop-> ⑥指令[imul] 彈出操作數(shù)棧中的兩個數(shù)據(jù)1和2,相乘之后的結(jié)果2堆入操作數(shù)棧 局部變量區(qū) 操作數(shù)棧 index hex value value offset hex value value (變量i) 0 00000001 1 optop-> 0 00000002 2 1 ⑦指令[istore_0] 彈出操作數(shù)棧頂?shù)臄?shù)據(jù)2,將結(jié)果存儲在局部變量區(qū)中index=0的空間中。 局部變量區(qū) 操作數(shù)棧 index hex value value offset hex value value (變量i) 0 00000002 2 optop-> 0 1 ⑧指令[goto 2] 跳轉(zhuǎn)到指令iinc 0 1處循環(huán)執(zhí)行下去.....
當(dāng)然,這個例子不停的執(zhí)行下去只會出現(xiàn)算術(shù)溢出,也就是一個字長(2bytes)的整型變量i 無法表示不停計(jì)算的結(jié)果了。但是JVM不會拋出任何異常,
附:在《深入Java虛擬機(jī)》一書第5章節(jié)有一個JVM模擬運(yùn)行上面程序的源代碼和applet展示,做的很不錯。下面是這本書的配到源代碼,大家可以學(xué)習(xí)一下。 |
|