眾所周知,Java源代碼被編譯器編譯成class文件。而并不是底層操作系統可以直接執行的二進制指令(比如Windows OS的.exe文件)。因此,我們需要有一種平臺可以解釋class文件并運行它。而做到這一點的正是Java 虛擬機(JVM)。
?
實際上,
JVM是一種解釋執行class文件的規范技術。
各
個提
供商都可以根據規范,在不同的底層平臺上實現不同的JVM。
?
下面是JVM實現的基本結構框圖。其中
類裝載子系統、運行時數據區、執行引擎
等
是JVM的必須要解決的幾大問題。
★ 類裝載器子系統
在JVM中,類裝載器子系統負責查找并裝載Class文件。關于這部分的裝載細節詳見《
JVM加載class文件的原理
》
★ 運行時數據區
當Java虛擬機運行一個程序時,需要內存在存儲許多東西。比如字節碼,程序創建的對象,傳遞的方法參數,返回值,局部變量等等。JVM會把這些東西都組織到幾個“運行時數據區”中便于管理。
(1) 方法區
當JVM使用類裝載器定位Class文件,并將其輸入到內存中時。會提取Class文件的類型信息,并將這些信息存儲到方法區中。同時放入方法取中的還有該類型中的類靜態變量。下面我們具體看看需要存儲哪些信息?
●該類型的全限定名。如java.io.FileOutputStream
●該類型的直接超類的全限定名。如java.io.OutputStream
●該類型是類類型還是接口類型。
●該類型的訪問修飾符(public、abstract、final)。
●任何直接超接口的全限定名的有序列表。如java.io.Closeable,? java.io.Flushable。
●該類型的常量池。比如所有類型、方法和字段的符號。基本數據類型的直接數值等。
●字段信息。對類型中聲明的每個字段,方法區中必須保存下面的信息。除此之外,這些字段在類或者接口中的聲明順尋也必須保存。
????? 字段名
????? 字段的類型
????? 字段的修飾符(public, private, protected, static, final, volatile, transient的某個子集)
●方法信息。和字段一樣保存方法的相關信息。
????? 方法名
????? 方法的返回類型
????? 方法的參數的數量和類型
????? 方法的修飾符
????? 方法的字節碼
????? 操作數棧和棧幀中局部變量的大小 (見下面Java棧的內容)
????? 異常表
●類靜態變量。這里要注意:類中的靜態變量時存放在方法區中的。并不是存放在堆中某一個該類型的對象中的。
也就是我們常說的“類靜態變量屬于類,而不屬于對象”這句話的由來了。
●指向ClassLoader類的引用。任何類都需要被類裝載器裝入內存。如果是被用戶自定義類裝載器裝載的,那么JVM必須在類型信息中存儲對該裝載器對象的引用。
●指向Class類的 引用。對于每一個被裝載的類型,虛擬機都會相應的為它創建一個java.lang.Class類的實例,而且虛擬機還必須以某種方式把這個實例和存儲在方 法區中的類型信息關聯起來。 這就使得我們可以在程序運行時查看某個加載進內存的類的當前狀態信息。也就是反射機制實現的根本。 ●方法表。 為了能快速定位到類型中的某個方法。JVM對每個裝載的類型都會建立一個方法表,用于存儲該類型對象可以調用的方法的直接引用,這些方法就包括從超類中繼 承來的。而這張表與 Java動態綁定機制( 參見《 java動態綁定機制實現多態 》 )的實現是密切相關的。 ? ?
方法區是多線程共享的。也就是當虛擬機實例開始運行程序時,邊運行邊加載 進Class文 件。不同的Class文件都會提取出不同類型信息存放在方法區中。同樣,方法區中不再需要運行的類型信息會被垃圾回收線程丟棄掉。右圖形象的顯示出了方法區的樣子。
|
|
?
(2) 堆
Java 程序在運行時創建的所有類型對象和數組都存儲在堆中。JVM會根據new指令在堆中開辟一個確定類型的對象內存空間。但是堆中開辟對象的空間并沒有任何 人工 指令可以回收,而是通過JVM的垃圾回收器負責回收。
?
堆中對象存儲的是該對象以及對象所有超類的實例數據(但不是靜態數據), 比如下面的類型:
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的內存空間。其中x1中的一個實例數據data=100,而x2的data=200。但是這兩個對象中都沒有stcdata這樣的數據,這個靜態數據存儲在上面講到的方法區中。
?
此外,堆中對象還必須有指向方法區中的類信息數據(見上面方法區)。 為什么需要這個信息呢?因為當程序在運行時需要對象轉型,那么JVM必須檢查當前對象所屬類型及父類的信息。以判斷轉型是否是合法的,而這一點也是instanceof操作符實現的基礎。
?
當然,上述只是JVM的規范,具體堆的實現是由JVM設計者來決定。下面兩幅圖就直觀的表現出了堆對象的不同實現結構:
其中一個對象的引用可能在整個運行時數據區中的很多地方存在,比如Java棧,堆,方法區等。
?
堆中對象還應該關聯一個對象的鎖數據信息以及線程的等待集合。 這些都是實現Java線程同步機制的基礎。但實際上很多具體實現中并不在對象自身內部保存一個指向鎖數據的指針。而只有當第一次需要加鎖的時候才分配對應鎖數據。另外,每個對象都會從Object中繼承三個Object方法(wait、notify、notifyAll),當某個線程在一個對象上調用了等待方法時。JVM就會阻塞這個線程,并把這個線程放在該對象的等待集合中。知道另外一個線程在該對象上調用了notify/notifyAll,JVM才會在等待集合中喚醒一個或全部的等待線程(參見《 正確理解線程等待和釋放(wait/notify) 》)。
?
【數組對象】
在Java中,數組也是對象,那么自然在堆中會存儲數組的信息。事實也確實如此,對于JVM而言,數組與其他類對象沒有任何區別。
數組也有屬于的類Class,具有相同維度和類型的數組都是同一個類的實例,而不管數組的長度是多少。
數組類的名稱由兩部分構成:(1)每一維用一個方括號“[”表示。(2) 用字符或字符串表示元素類型。比如一維數組對象int[] a所屬類型名為"[I",二維數組對象byte[] b所屬類型名為"[[B"。
下圖是二維數組對象在堆中的具體實現方式:
?
?
(3) 程序計數器
?
對于一個運行的Java而言,每一個線程都有一個PC寄存器。當線程執行Java程序時,PC寄存器的內容總是下一條將被執行的指令地址。
?
(4) Java棧 -? 棧幀
?
每啟動一個線程,JVM都會為它分配一個Java棧,用于存放方法中的局部變量,操作數以及異常數據等。當線程調用某個方法時,JVM會根據方法區中該方法的字節碼組建一個棧幀。并將該棧幀壓入Java棧中,方法執行完畢時,JVM會彈出該棧幀并釋放掉。
注意,Java棧中的數據是線程私有的,一個線程是無法訪問另一個線程的Java棧的數據。這也就是為什么多線程編程時,兩個相同線程執行同一方法時,對方法內的局部變量時不需要數據同步的原因。
?
【 棧幀 】
棧幀有三部分構成:局部變量區、操作數棧和幀數據區。在編譯器編譯Java代碼時,就已經在字節碼中為每個方法都設置好了局部變量區和操作數棧的數據和大小。并在JVM首次加載方法所屬的Class文件時,就將這些數據放進了方法區。因此在線程調用方法時,只需要根據方法區中的局部變量區和操作數棧的大小來分配一個新的棧幀的內存大小,并堆入Java棧。
?
局部變量區: 用來存放方法中的所有局部變量值,包括傳遞的參數。這些數據會被組織成以一個字長(32bit或64bit)為單位的數組結構(以索引0開始)中。其中類型為int, float, reference(引用類型,記錄對象在堆中地址)和returnAddress(一種JVM內部使用的基本類型)的值占用1個字長,而byte, char和shot會擴大成1個字長存儲,long,double則使用2個字長。
?
操作數棧: 用來在執行指令的時候存儲和使用中間結果數據。
?
幀數據區: 常量池的解析,正常方法返回以及異常派發機制的信息數據都存儲在其中。
?
下圖展示了addAndPrint()調用addTwoTypes()時,Java棧的變化:
?
★ 執行引擎
運行Java的每一個線程都是一個獨立的虛擬機執行引擎的實例。從線程生命周期的開始到結束,他要么在執行字節碼,要么在執行本地方法。一個線程可能通過解釋或者使用芯片級指令直接執行字節碼,或者間接通過JIT執行編譯過的本地代碼。
?
指令集: 實際上,Class文件中方法的字節碼流就是有JVM的指令序列構成的。每一條指令包含一個單字節的操作碼,后面跟隨0個或多個操作數。
?
Java虛擬機指令集關注的中心是操作數棧和局部變量集合。我們可以看看下面一組指令在執行引擎中執行的過程:
iload_0 // 把存儲在局部變量區中索引為0的整數壓入操作數棧。 iload_1 // 把存儲在局部變量區中索引為1的整數壓入操作數棧。 iadd // 從操作數棧中彈出兩個整數相加,在將結果壓入操作數棧。 istore_2 // 從操作數棧中彈出結果
很顯然,上面的指令反復用到了Java棧中的某一個方法棧幀。實際上執行引擎運行Java字節碼指令很多時候都是在不停的操作Java棧,也有的時候需要在堆中開辟對象以及運行系統的本地指令等。但是Java棧的操作要比堆中的操作要快的多,因此反復開辟對象是非常耗時的。這也是為什么Java程序優化的時候,盡量減少new對象。
?
?
?
?
下面將會是很有趣的過程,我們用一段代碼來生動的展現JVM是如何運行這段程序的。
?
通過編譯器將下面的代碼編譯成edu/hr/jvm/Test.class 和 edu/hr/jvm/bean/Act.class。然后開始啟動JVM:
//源代碼 Test.java package edu.hr.jvm; import edu.hr.jvm.bean; public class Test{ public static void main(String[] args){ Act act=new Act(); act.doMathForever(); } } //源代碼 Act.java package edu.hr.jvm.bean; public class Act{ public void doMathForever(){ int i=0; for(;;){ i+=1; i*=2; } } }
?
(1) 首先OS會創建一個JVM實例(進行必要的初始化工作,比如初始啟動類裝載器,初始運行時內存數據區等)。 ? (2) 然后通過 自定義類裝載器 加載Test.class。并提取Test.class字節碼中的信息存放在 方法區 中(具體的信息在上面已經講過)。右圖展示了方法區中的Test類信息,其中在常量池中有一個符號引用"Act"(注意:這個引用目前還沒有真正的類信息 的內存地址)。 ? (3) 接著JVM開始從Test類的main字節碼處開始解釋執行。在運行之前,會在Java棧中組建一個main方法的 棧幀 。如右圖Java棧所示。JVM需要運行任何方法前,通過在 Java棧 中壓入一個幀棧。在這個幀棧的內存區域中進行計算。 ? (4) 現在可以開始執行main方法的第一條指令——JVM需要為 常量池 的第一項的類(符號引用Act)分配內存空間。但是Act類此時還沒有加載進JVM(因為常量池目前只有一個"Act"的符號引用)。 |
|
?
(5) JVM加載進Act.class,并提取Act 類信息 放入方法區中。見上圖方法區所示,然后以一個直接指向方法區Act類信息的直接引用替換開始在常量池中的符號引用"Act",這個過程就是 常量池解析 。以后就可以直接訪問Act的類信息了。
?
(6) 此時JVM可以根據方法區中的Act類信息,在堆中開辟一個Act 類對象 act。見上圖堆所示。
?
(7) 接著開始執行main方法中的第二條指令調用doMathForever。這個可以通過堆中act對象所指的 方法表 中查找,然后定位到方法區中的Act類信息中的doMathForever方法字節碼。在運行之前,仍然要組建一個doMathForever棧幀壓入Java棧,如上圖所示。(注意:JVM會根據方法區中doMathForever的字節碼來創建棧幀的局部變量區和操作數棧的大小)
?
(8) 接下來JVM開始解釋運行Act.doMathForever字節碼的內容了。下面我們詳細的描述一下這個JVM的運行過程: ● 我們首先看一下doMathForever方法的字節碼在方法區中的指令如右圖,其中bytecode是指令的 二進制編碼 ,mnemonic是 指令助記符 ,pc為 程序計數器 (指向當前運行指令的下一條),offset為指令存放在方法區中的 地址偏移 。 ? ●然后在上面的圖Java棧中已經顯示出了doMathForever方法的棧幀,其中比較重要的兩個部分是 局部變量區 和 操作數棧 。而此時在運行指令之前,局部變量區中只有一個整型i 的存儲位置(1個字長)。而操作數棧中還沒有被創建了2個字長的大小(存儲大小是幀棧創建的時候由方法區中的數據確定的)。 ? ?????????????????????? ?????? 局部變量區 ???????????? index??????? hex value??????? value
?
(變量i)
???? 0???????????????????????????????????????????????
?
?
optop: 0????????? ? ? ?? 操作數棧?????????
????????????? offset??????? hex value??????? value
?optop->??? 0 ????????????????? 1 ? |
|
?
●? 下面運行每一條指令后,看一下局部變量區和操作數棧的變化:
??? ① 指令[iconst_0]?? 將int類型變量的數據0壓入操作數棧。
??????????????????????
?????? 局部變量區?????????????????????????????????????
? ? ? ? ? ? ???????? ? ? ?? 操作數棧????
???????????? index??????? hex value??????? value???????????????????????????????? offset??????? hex value??????? value
??
(變量i)
??? 0????????????????????????????????????????????????????????????????
???????????????? 0?????????? 00000000????????? 0
?????????????????????????????????????????????????????????????????????????????????? optop-> ?? 1
??? ② 指令[istore_0]??? 彈出操作數棧頂的數據0,將結果存儲在局部變量區中index=0的空間中。
??????????? ??????????
?????? 局部變量區?????????????????????????????????????
? ? ? ? ? ? ???????? ? ? ?? 操作數棧????
???????????? index??????? hex value??????? value???????????????????????????????? offset??????? hex value??????? value
??
(變量i)
??? 0???????? ?? 00000000 ? ? ? ?? 0?????????????????????????
optop->
? 0
???????????????????????????????????????????????????????????????????????????????????????????????? ?? 1
??? ③指令[iinc 0 1]? 把常量值1加到局部變量區中index=0的空間上。
?
???????????????????????????? 局部變量區?????????????????????????????????????
? ? ? ? ? ? ???????? ? ? ?? 操作數棧????
???????????? index??????? hex value??????? value???????????????????????????????? offset??????? hex value??????? value
??
(變量i)
??? 0???????? ?? 00000001 ? ? ? ?? 1? ???????????????????????
optop->
? 0
???????????????????????????????????????????????????????????????????????????????????????????????? ?? 1
??? ④指令[iload_0]? 把局部變量區index=0中的數據堆入操作數棧。
?
???????????????????????????? 局部變量區?????????????????????????????????????
? ? ? ? ? ? ???????? ? ? ?? 操作數棧????
???????????? index??????? hex value??????? value???????????????????????????????? offset??????? hex value??????? value
??
(變量i)
??? 0???????? ?? 00000001 ? ? ? ?? 1? ???????????????????????????????????????
0???????? 00000001????????? 1
???????????????????????????????????????????????????????????????????????????????????? optop-> ? 1
??? ⑤指令[iconst_2] 把int類型變量的數據2壓入操作數棧。
?????????????????????????????? 局部變量區?????????????????????????????????????
? ? ? ? ? ? ???????? ? ? ?? 操作數棧????
???????????? index??????? hex value??????? value???????????????????????????????? offset??????? hex value??????? value
??
(變量i)
??? 0???????? ?? 00000001 ? ? ? ?? 1? ???????????????????????????????????????
0???????? 00000001????????? 1
??????????????????????????????????????????????????????????????????????????????????? ? ? ? ? ? ? ?
?
1???????? 00000002?????????? 2
???????????????????????????????????????????????????????????????????????????????????? optop->
??? ⑥指令[imul]? 彈出操作數棧中的兩個數據1和2,相乘之后的結果2堆入操作數棧
?????????????????????????????? 局部變量區?????????????????????????????????????
? ? ? ? ? ? ???????? ? ? ?? 操作數棧????
???????????? index??????? hex value??????? value???????????????????????????????? offset??????? hex value??????? value
??
(變量i)
??? 0???????? ?? 00000001 ? ? ? ?? 1? ????????????????????????
optop->
?
0???????? 00000002 ????? ? ? 2
??????????????????????????????????????????????????????????????????????????????????? ? ? ? ? ? ? ?
?
1?????
???? ⑦指令[istore_0]? 彈出操作數棧頂的數據2,將結果存儲在局部變量區中index=0的空間中。
?????????????????????????????? 局部變量區?????????????????????????????????????
? ? ? ? ? ? ???????? ? ? ?? 操作數棧????
???????????? index??????? hex value??????? value???????????????????????????????? offset??????? hex value??????? value
??
(變量i)
??? 0???????? ?? 00000002 ? ? ? ?? 2? ????????????????????????
optop->
?
0????????
??????????????????????????????????????????????????????????????????????????????????? ? ? ? ? ? ? ? ? 1?????
???? ⑧指令[goto 2] ? 跳轉到指令iinc 0 1處循環執行下去.....
?
當然,這個例子不停的執行下去只會出現算術溢出,也就是一個字長(2bytes)的整型變量i 無法表示不停計算的結果了。但是JVM不會拋出任何異常,
?
?
附:在《深入Java虛擬機》一書第5章節有一個JVM模擬運行上面程序的源代碼和applet展示,做的很不錯。下面是這本書的配到源代碼,大家可以學習一下。
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
