原文:Understanding JVM Internals by Se Hoon Park On 05/30/2017
翻譯:碼代碼的陳同學
翻譯參考:java字節(jié)序、主機字節(jié)序和網絡字節(jié)序掃盲貼
眾所周知头镊,Java字節(jié)碼運行在JRE(Java Runtime Environment)中瓦糕,JVM又是JRE中最重要的部分草丧,主要用于分析和執(zhí)行字節(jié)碼盆耽。雖然不深入了解JVM腋寨,開發(fā)人員也已經開發(fā)出許多優(yōu)秀的應用和Library聪铺,但如果了解JVM,你可以更好的理解Java語言萄窜,同時也可以解決一些看上去很簡單卻不好解決的問題铃剔。
因此撒桨,本文我將闡述JVM如何工作、JVM的結構键兜、JVM如何執(zhí)行字節(jié)碼以及執(zhí)行的順序凤类,常見的錯誤及其解決方案,也會介紹下Java SE 7的新特性普气。
虛擬機(Virtual Machine)
JRE由Java API和JVM組成谜疤,JVM的作用是通過Class Loader加載Java程序并通過Java API來執(zhí)行加載的程序
虛擬機可以像物理機一樣運行程序,它是通過軟件的方式來模擬實現的機器(例如計算機)现诀。Java被設計成基于虛擬機運行的初衷是希望通過和物理機分離以達到 WORA(Write One Run Anywhere)的目標夷磕,盡管這個目標早已被淡忘。正因如此仔沿,JVM才可以既不改變Java代碼卻又能運行在各種硬件上坐桩。
JVM的特性如下:
- 基于棧的虛擬機(Stack-Based VM):Intel x86 和ARM這兩種最為流行的架構都是基于寄存器運行的,然而JVM卻是基于Stack運行封锉。
- 符號引用(Symbolic reference):除基本數據類型外绵跷,所有的數據類型(類和接口)都是通過符號引用來引用,而不是通過具體的內存地址來引用成福。
- 垃圾回收(Garbage Collection):對象由用戶編寫的代碼創(chuàng)建碾局,由垃圾回收機制自動銷毀。
- 通過對基本數據類型的明確定義來保證平臺獨立性:像C/C++這種傳統語言闷叉,int類型的長度取決于平臺擦俐。JVM明確定義了基本數據類型來確保它的兼容性和獨立性。
- 網絡字節(jié)序(Network byte order):Java class文件使用了網絡字節(jié)序握侧,為了在小端字節(jié)序(如Intel x68體系)和大端字節(jié)序(如RISC系列體系)之間維持平臺獨立性蚯瞧,必須保證固定的字節(jié)順序。因此品擎,JVM使用了用于網絡傳輸的網絡字節(jié)序埋合,網絡字節(jié)序屬于大端。
雖然Sun公司開發(fā)了Java萄传,但是所有JVM提供商都可以基于JVM規(guī)范開發(fā)自己的JVM甚颂。正因如此,市面上有許多不同的虛擬機秀菱,包含Oracle的HotSpot JVM和IBM JVM振诬。Google 安卓操作系統中的Dalvik虛擬機也是一種JVM,盡管它沒有基于JVM規(guī)范衍菱,不像基于Stack的Java虛擬機赶么,Dalvik虛擬機是基于寄存器的架構,Dalvik虛擬機會將Java字節(jié)碼轉換成基于寄存器的指令集脊串。
字節(jié)碼(Java bytecode)
為了實現 WORA 目標辫呻,JVM使用字節(jié)碼這種介于Java(用戶語言)和機器語言之間的中間語言清钥,字節(jié)碼是部署Java代碼的最小單位。
在解釋Java字節(jié)碼之前放闺,讓我們先看看它的樣子祟昭。下面是一個開發(fā)過程中遇到的真實案例總結:
現象
一個一直運行的程序在某個依賴的Library被更新后發(fā)生了如下錯誤.
譯者注:為了便于理解,譯者舉個例子怖侦。例如:一個應用的war包沒做任何變更篡悟,但是替換了某個依賴的jar包。
Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
at com.nhn.service.UserService.add(UserService.java:14)
at com.nhn.service.UserService.main(UserService.java:19)
程序代碼如下础钠,而且沒有任何變更.
// UserService.java
…
public void add(String userName) {
admin.addUser(userName);
}
Library中被更新的部分的前后代碼如下:
// UserAdmin.java - Updated library source code
…
public User addUser(String userName) {
User user = new User(userName);
User prevUser = userMap.put(userName, user);
return prevUser;
}
// UserAdmin.java - Original library source code
…
public void addUser(String userName) {
User user = new User(userName);
userMap.put(userName, user);
}
可以發(fā)現恰力,addUser()
方法從沒有返回值改成了返回User對象叉谜,并且程序代碼沒有做任何變更旗吁,因為程序中并沒有用到這個返回值⊥>郑看上去addUser()
方法也存在很钓,那為什么還要報 NoSuchMethodError 呢?
原因
原因在于應用程序的代碼沒有基于新的Library重新編譯董栽,換句話說码倦,程序中還是執(zhí)行了正確的方法,只是沒有返回值而已锭碳。然而袁稽,編譯后的class文件卻表明這個方法是有返回值的∏芘祝可以通過下面的錯誤消息來了解:
java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
由于找不到方法報了NoSuchMethodError
推汽,看一下 Ljava/lang/String;和后面的 V,在Java字節(jié)碼表達式中歧沪,L<classname>; 表示類實例歹撒, Ljava/lang/String;表示方法有一個String類型的參數。在上面的例子中诊胞,參數沒有變化暖夭,所以是正常的。最后的V表示方法的返回值撵孤,只有一個V表示沒有返回值迈着。上述異常消息表示沒有找到這個方法。
由于程序代碼是根據以前的Library編譯的邪码,class文件中并沒有定義有返回值的addUser()方法裕菠。然而,在Library變更后霞扬,addUser()更新成了返回一個User的方法糕韧。因此枫振,發(fā)生了 NoSuchMethodError。
注:這個錯誤的發(fā)生是由于開發(fā)人員沒有使用新的Library重新編譯應用萤彩,但是玖姑,這種場景下,Library的提供者更應該為此負責删咱。一個公共的沒有返回值的方法變更成了一個返回一個對象的方法荒叶,這顯然是變更類的簽名信息,這也意味著打破了這個Library的向后兼容性愚墓。因此予权,Library的提供者必須告知使用者Library發(fā)生了變更。
讓我們回到Java字節(jié)碼浪册,Java字節(jié)碼是JVM的基本元素扫腺。JVM是一個模擬執(zhí)行字節(jié)碼的模擬器,Java編輯器不會將高級語言(如C/C++)轉換成機器語言(CPU指令)村象,它會將開發(fā)人員可以理解的Java語言轉換成JVM可以理解的Java字節(jié)碼笆环。由于Java字節(jié)碼是平臺無關的代碼,因此即使CPU或操作系統不同厚者,它也可以運行在所有安裝了JVM(準確的說躁劣,是與硬件匹配的JRE)的硬件上(一個class文件在 Windows PC上編譯后不做任何改變就可以運行在Linux上)。編譯后的字節(jié)碼和源代碼的大小基本一致库菲,這樣可以更容易的在網絡上傳輸和執(zhí)行編譯后的代碼账忘。
class文件本身是一個開發(fā)人員無法理解的二進制文件,為了管理這些文件熙宇,JVM提供商提供了反匯編器javap鳖擒,javap產生的結果是Java匯編語言。下面的代碼是通過javap命令產生的:
public void add(java.lang.String);
Code:
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V
8: return
在這段匯編代碼中奇颠,addUser()方法是在第4行的5: invokevirtual #23;
執(zhí)行的败去,這表示對應的索引為23的方法會被執(zhí)行,#23是由javap進行標記的烈拒。invokevirtual是Java字節(jié)碼中最基本的操作碼圆裕,一共有4種執(zhí)行方法的操作碼:invokeinterface, invokespecial, invokestatic, 和 invokevirtual.,其含義如下:
- invokeinterface: 執(zhí)行一個接口的方法
- invokespecial: 執(zhí)行一個初始化方法荆几,私有方法或父類中的方法
- invokestatic: 執(zhí)行靜態(tài)方法
- invokevirtual:執(zhí)行對象實例中的方法
Java字節(jié)碼的指令集由操作碼(OpCode)和操作數(Operand)組成吓妆,像 invokevirtual 這樣的操作碼需要2字節(jié)的操作數。
通過編譯上面例子中變更后的代碼再反匯編得到的結果如下:
public void add(java.lang.String);
Code:
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
8: pop
9: return
你會發(fā)現標記為#23的方法返回的是 Lcom/nhn/user/User;吨铸。
那上面的匯編代碼中行拢,最前面的數字又是什么意思呢?
這是字節(jié)數(byte number)诞吱,也許這就是為什么JVM中執(zhí)行的代碼叫字節(jié)碼的原因舟奠。簡而言之竭缝,字節(jié)碼指令中的像 aload_0, getfield, 和 invokevirtual 這些操作碼都是用1個字節(jié)來表示(aload_0 = 0x2a, getfield = 0xb4, invokevirtual = 0xb6)。因此沼瘫,Java字節(jié)碼指令的操作碼最多有256個抬纸。
像aload_0、aload_1這種操作碼不需要任何操作數耿戚,因此湿故,aload_0后面的下一個字節(jié)會是下一條指令的操作碼。然后膜蛔,getfield和invokevirtual需要2個字節(jié)的操作數坛猪,因此,getfield之后的下一條指令會跳過2字節(jié)皂股,是寫在第4個字節(jié)上墅茉。下面是通過16進制編輯器看到的字節(jié)碼:
2a b4 00 0f 2b b6 00 17 57 b1
在Java字節(jié)碼中,"L"表示類實例屑墨,"V"表示void躁锁,其他類型也有它們自己的表達式,下面是字節(jié)碼中的其他表達式:
Java Bytecode | Type | Description |
---|---|---|
B | byte | signed byte |
C | char | Unicode character |
D | double | double-precision floating-point value |
F | float | single-precision floating-point value |
I | int | integer |
J | long | long integer |
L<classname> | reference | an instance of class <classname> |
S | short | signed short |
Z | boolean | true or false |
[ | reference | one array dimension |
下面是Java字節(jié)碼表達式的簡單例子:
Java Code | Java Bytecode Expression |
---|---|
double d[][][]; | [[[D |
Object mymethod(int I, double d, Thread t) | (IDLjava/lang/Thread;)Ljava/lang/Object; |
class文件格式(Class File Format)
在解釋Java class文件格式之前卵史,我們先回顧下在Java Web應用中經常出現的情景。
現象
在Tomcat上運行JSP時搜立,JSP代碼并沒有正常運行以躯,而是報了如下錯誤:
Servlet.service() for servlet jsp threw exception org.apache.jasper.JasperException: Unable to compile class for JSP Generated servlet error:
The code of method _jspService(HttpServletRequest, HttpServletResponse) is exceeding the 65535 bytes limit
原因
這個錯誤消息在不同Web應用服務器上可能稍微不同,但是有一點是相同的啄踊,那就是65535字節(jié)的限制忧设。65535字節(jié)是JVM的一個限制,用來保證一個方法的 size不能超過65535字節(jié)颠通。
我將詳細的說明65535字節(jié)限制的意義以及為什么設置了這個限制址晕。
Java字節(jié)碼中的分支和跳轉指令分別是 goto 和 jsr:
goto [branchbyte1] [branchbyte2]
jsr [branchbyte1] [branchbyte2]
這兩個指令都接收一個2字節(jié)的分支偏移量(有符號數)作為它們的操作數,因此偏移量最大為65535(2字節(jié)為16位)顿锰。然而谨垃,為了支持更多的分支,Java字節(jié)碼準備了 goto_w 和 **jsr_w **這兩個可以接收4字節(jié)有符號數分支偏移量的指令硼控。
goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]
jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]
通過這兩個指令刘陶,索引超過65535的分支也是OK的,這樣Java方法65535字節(jié)的限制也許就搞定了牢撼。然而匙隔,由于class文件格式的其他限制,Java方法還是不能超過65535字節(jié)熏版。為了了解其他限制纷责,我先簡單介紹下class文件的格式捍掺。
下面是class文件的格式:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];}
譯者注:各部分的含義暫不翻譯,有興趣的可以參考《深入理解Java虛擬機》這本書再膳。
javap以用戶可讀的格式簡要的展示class文件的信息乡小,使用javap -verbose
命令分析UserService.class 得到的數據如下:
Compiled from "UserService.java"
public class com.nhn.service.UserService extends java.lang.Object
SourceFile: "UserService.java"
minor version: 0
major version: 50
Constant pool:const #1 = class #2; // com/nhn/service/UserService
const #2 = Asciz com/nhn/service/UserService;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz admin;
const #6 = Asciz Lcom/nhn/user/UserAdmin;;// … omitted - constant pool continued …
{
// … omitted - method information …
public void add(java.lang.String);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
8: pop
9: return LineNumberTable:
line 14: 0
line 15: 9 LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/nhn/service/UserService;
0 10 1 userName Ljava/lang/String; // … Omitted - Other method information …
}
由于篇幅限制,我抽取了整個輸出信息的一部分饵史,整個輸出信息中還包含了常量池和每個方法的內容等信息满钟。
方法65535字節(jié)的限制和 method_info_struct 有關,method_info 結構包含了Code胳喷,LineNumberTable和LocalVariableTable湃番,如上述 javap -verbose
命令輸出的信息。Code屬性里的LineNumberTable吭露、LocalVariableTable吠撮、Exception_table的長度都是用一個固定的2字節(jié)來表示,因此讲竿,方法的大小不能超過它們長度65535的限制泥兰。
許多人抱怨方法長度的限制,盡管JVM規(guī)范里表明后續(xù)會對此進行拓展题禀,但到目前為止也沒有任實質性的進展鞋诗。考慮到JVM把class文件中的很多內容放到了方法區(qū)迈嘹,為了保證向后兼容性削彬,拓展方法的長度將愈加艱難。
如果由于Java編譯錯誤創(chuàng)建了錯誤的class文件會發(fā)生什么呢秀仲?或者融痛,因為網絡傳輸或文件拷貝時class文件被損壞呢?
為了預防這種情況神僵,JVM class loader有一個非常嚴格的校驗過程雁刷。
JVM結構(JVM Structure)
Java中編寫的代碼通過以下流程來執(zhí)行。
一個Class Loader裝載編譯后的字節(jié)碼到運行時數據區(qū)保礼,然后執(zhí)行引擎用于執(zhí)行Java字節(jié)碼沛励。
類裝載器(Class Loader)
Java提供了一種動態(tài)裝載特性,它可以在運行時首次引用某個class時對它進行裝載和鏈接氓英,而不是在編譯時進行侯勉。JVM的class loader用于進行動態(tài)裝載,下面是class loader的幾個特性:
- 層級結構(Hierarchical Structure): Java里的class loader被組織成了有父子關系的層級結構铝阐。Bootstrap class loader是所有class loader的父類址貌。
- 委派模型(Delegation mode): 基于層級結構,類的裝載可以在class loaders之間進行委派。當一個class裝載之時练对,會首先檢查它是否在父裝載器中進行了裝載遍蟋。如果上層裝載器已經裝載了這個類,將會直接使用這個類螟凭;如果沒有虚青,當前類裝載器將會請求裝載這個類。
- 可見性限制(Visibility limit): 一個子裝載器可以查找父裝載器中的類螺男,但是父裝載器不能查找子裝載器中的類棒厘。
- 不允許卸載(Unload is not allowed): 類裝載器可以裝載一個類但是不能卸載它,不過可以刪除當前類裝載器下隧,然后創(chuàng)建一個新的類裝載器奢人。
每一個class loader都一個自己的命名空間來保存裝載的類,當一個class loader裝載一個類時淆院,它會使用類的全限定名(FQCN: Fully Qualified Class Name)去命名空間中查找類是否被裝載何乎。需要注意的是即使類的全限定名相同,但如果命名空間不同土辩,也會被認為是不同的類支救,命名空間不同意味著類已經被其他class loader裝載了。
下圖演示了class loader的委派模型:
當一個class loader請求加載一個class時拷淘,它首先按順序在上層裝載器各墨、父裝載器以及自身的裝載器緩存中檢查類是否已存在。簡單來說辕棚,首先會檢查自己是否裝載了該類欲主,如果沒有將繼續(xù)檢查父裝載器,最后如果在Bootstrap裝載器中都沒有找到的話逝嚎,將會從文件系統裝載這個類。
- 啟動類裝載器(Bootstrap class loader): 它在JVM啟用時創(chuàng)建详恼,它負責裝載Java APIs补君,包含相關對象的class。不像其他class loader昧互,這個類裝載器由native代碼實現挽铁,而不是Java代碼。
- 拓展類裝載器(Extension class loader): 它負責裝載除了Java API外的拓展類敞掘,也負責裝載其他安全拓展功能叽掘。
- 系統類裝載器(System class loader): 如果說bootstrap class loader和 extension class loader是用來裝載JVM組件的,那system class loader就是用來裝載應用程序的類玖雁。它會裝載用戶在$CLASSPATH中指定的類更扁。
- 用戶自定義類裝載器(User-defined class loader): 這是用戶在程序中直接創(chuàng)建的類裝載器。
Web應用服務器(WAS)等框架會使用這種結構讓Web應用和企業(yè)級應用保持獨立運行,換句話說浓镜,通過class loader的委派模型來保證應用的獨立性溃列,不同WAS服務商的class loader在層級結構上可能稍有不同。
如果一個class loader找到了一個未裝載的類膛薛,這個類裝載和鏈接的流程如下圖:
- 加載(Loading): 從class文件獲取類信息并加載到JVM內存
- 驗證(Verifying): 檢查class是否符合Java語言規(guī)范和JVM規(guī)范听隐,這是類裝載中最為復雜的流程,會花費很長時間哄啄。大多數JVM TCK測試case就是用來測試在裝載類的時候是否會出現錯誤
- 準備(Preparing): 準備一個數據結構用來存儲類信息雅任,結構中包含:類的成員變量、方法和接口信息咨跌。
- 解析(Resolving): 將這個類的常量池中所有的符號引用換成直接引用沪么。
- 初始化(Initializing): 將類的成員變量初始化成合適的值,執(zhí)行靜態(tài)初始化程序虑润,把靜態(tài)變量初始化成合適的值成玫。
JVM規(guī)范中定義了上面幾個任務,但是在執(zhí)行時可以進行靈活的變動拳喻。
運行時數據區(qū)(Runtime Data Area)
運行時數據區(qū)是JVM程序運行在操作系統上時的內存分配區(qū)域哭当,它可以分稱6個部分:程序計數器,JVM棧冗澈、本地方法棧都是為每個線程創(chuàng)建的钦勘,堆、方法區(qū)亚亲、運行時常量池是所有線程共享的彻采。
程序計數器(PC register): 每個線程有自己的程序計數器 (Program Counter) , 它在線程start的時候創(chuàng)建。.程序計數器保存了當前正在執(zhí)行的JVM指令的地址捌归。
JVM stack: 每個線程擁有一個JVM棧 , 它在線程start的時候創(chuàng)建肛响。主要用來保存棧幀,JVM只會在Stack上進行棧幀的push和pop操作惜索。如果發(fā)生任何異常特笋,stack中的每一行都代表一個棧幀信息,這些信息可以通過像printStackTrace()這樣的方法展示出來巾兆。
本地方法棧(Native method stack): 提供給非Java語言寫的本地方法使用的stack猎物。換句話說,它是一個用于通過JNI(Java Native Interface)執(zhí)行C/C++代碼的stack角塑,根據具體的語言蔫磨,會創(chuàng)建一個C stack或C++ stack。
方法區(qū)(Method area): 方法區(qū)被所有線程共享圃伶,在JVM啟動時創(chuàng)建堤如,它存儲了運行時常量池蒲列、變量和方法信息,靜態(tài)變量煤惩,class中每個方法的字節(jié)碼以及接口信息嫉嘀。不同JVM提供商對于方法區(qū)有不同的實現,Oracle HotSpot JVM把它稱為永久區(qū)(Permanent Area)或永久代(Permanent Generation (PermGen))魄揉,是否對方法區(qū)進行垃圾回收對于JVM的實現來說也是可選的剪侮。
運行時常量池(Runtime constant pool): 這個區(qū)域和class文件中的常量池表(contant_pool table)對應,它屬于方法區(qū)洛退。由于在JVM操作中它卻扮演著核心角色瓣俯,因此JVM規(guī)范中單獨提到了它的重要性。除了每個類和接口中的常量兵怯,它也包含了方法和變量中的所有引用彩匕。簡而言之,當一個方法或變量被引用時媒区,JVM會從運行時常量區(qū)檢索方法或者變量的實際地址驼仪。
堆(Heap): 是用于保存實例和對象的空間,也是垃圾回收的主要區(qū)域袜漩。當討論JVM性能問題時绪爸,這個區(qū)域會頻繁提及。JVM提供商可以決定怎么配置堆或者不對它進行垃圾回收宙攻。
讓我們回到前面討論的反匯編的字節(jié)碼奠货。
public void add(java.lang.String);
Code:
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
8: pop
9: return
Java匯編代碼
對比Java匯編代碼和x86架構下的匯編代碼我們可以發(fā)現二者的格式有點相似:都有操作碼;然而座掘,有一點不同是在Java字節(jié)碼中不會在操作數中寫入寄存器名稱递惋、內存地址或偏移量。上面說過溢陪,JVM使用stack萍虽,因此,它不像x86架構直接使用寄存器形真,由于JVM自己管理內存贩挣,所以使用像15、23這樣的索引數來代替內存地址没酣。15和23都是當前類(UserService class)常量池中的索引。簡而言之卵迂,JVM為每個類創(chuàng)建了一個常量池用于存儲實際的引用裕便。
上面每一行反匯編代碼的含義如下:
- aload_0: 將局部變量表中索引為#0的變量添加到操作數棧,#0永遠表示的是this见咒,即當前實例的引用偿衰。
- getfield #15:將當前類常量池中索引為#15的變量添加到操作數棧,添加了UserAdmin的admin屬性,由于admin屬性是一個類的實例下翎,因此添加的是一個引用缤言。
-
aload_1: 將局部變量表中索引為#1的變量添加到操作數棧,局部變量表中#1位置的變量是一個方法的參數视事,因此胆萧,在執(zhí)行
add()
方法時,會將字符串參數userName的引用添加到操作數棧俐东。 -
invokevirtual #23: 執(zhí)行當前類常量池中索引為#23的方法跌穗,此時,通過
getfield
添加的引用和aload_1
添加的參數都將用于方法執(zhí)行虏辫。當方法執(zhí)行完后蚌吸,返回值將會添加到操作數棧。 -
pop: 使用
invokevirtual
指令將返回值從操作數棧中pop出來砌庄,最上面的例子中羹唠,通過最開始的Library編譯的方法是沒有返回值的,由于方法沒有返回值娄昆,因此沒有必要將返回值從stack中Pop出來佩微。 - return: 結束方法。
下圖可以輔助你理解上面的信息:
另外稿黄,在這個方法中喊衫,局部變量表并沒有發(fā)生變化,所以上圖只展示了操作數棧的變化杆怕。然而族购,在大多數場景中,局部變量表會發(fā)生變更陵珍,數據會通過一些load指令(如aload,iload)和store指令(如astore,istore)在局部變量表和操作數棧中進行傳輸寝杖。
在圖中,我們簡單演示驗證了運行時常量池和JVM Stack互纯。在JVM運行時瑟幕,類實例將在堆中進行分配,而像User留潦、UserAdmin只盹、UserService這些類信息和字符串將被存儲到方法區(qū)。
執(zhí)行引擎(Execution Engine)
字節(jié)碼通過class loader加載到JVM的運行時數據區(qū)兔院,然后由執(zhí)行引擎執(zhí)行殖卑。執(zhí)行引擎像CPU一條一條執(zhí)行機器碼一樣以指令為單位讀取Java字節(jié)碼,每個字節(jié)碼指令由1個字節(jié)的操作碼和附加的操作數組成坊萝。執(zhí)行引擎獲取一個操作碼再結合操作數來執(zhí)行任務孵稽,執(zhí)行完后再執(zhí)行下一條操作碼许起。
與可以被機器直接執(zhí)行的語言相比,字節(jié)碼是以一種人類可讀的語言來編寫菩鲜。因此园细,字節(jié)碼必須在JVM中轉換成一種可以被機器直接執(zhí)行的語言。字節(jié)碼可以通過以下兩種方式轉換成合適的語言:
- 解釋器(Interpreter):逐條讀取接校、解釋猛频、執(zhí)行字節(jié)碼指令。由于是逐條解釋馅笙、執(zhí)行指令伦乔,所以可以很快的解釋字節(jié)碼,但是執(zhí)行起來卻比較慢董习,這也是解釋型語言的缺陷烈和,字節(jié)碼這種"語言"基本上就是解釋執(zhí)行的。
- 即時編譯器(JIT(Just-In-Time) Compiler):JIT編譯器被引入用來彌補解釋器的不足皿淋。執(zhí)行引擎首先以解釋執(zhí)行的方式來運行招刹,然后在合適的時間,會把所有字節(jié)碼編譯成本地代碼(Native Code)窝趣,自此之后疯暑,執(zhí)行引擎可以直接執(zhí)行本地代碼,不再需要解釋執(zhí)行哑舒。本地代碼的執(zhí)行比逐條執(zhí)行字節(jié)碼快很多妇拯,尤其是本地代碼存儲到緩存之后,編譯后的代碼可以執(zhí)行的更快洗鸵。
然而越锈,與解釋器逐條解釋字節(jié)碼相比,JIT編譯器會消耗更多的時間用于編譯代碼膘滨。因此甘凭,如果代碼只會被執(zhí)行一次,最好用解釋執(zhí)行而不是編譯代碼火邓。正因如此丹弱,JVM內部會使用JIT編譯器檢查方法的執(zhí)行頻率,而且只會編譯執(zhí)行頻率超過一定水平的代碼铲咨。
圖7: Java編譯器和即時編譯器
JVM規(guī)范中并未定義執(zhí)行引擎如何運行躲胳,因此,不同JVM提供商會使用不同的技術來優(yōu)化他們的執(zhí)行引擎纤勒,也會引入不同種類的JIT編譯器泛鸟。
大多數的JIT編譯器以下圖的方式執(zhí)行:
圖8: 即時編譯器
JIT編譯器將字節(jié)碼轉換成中間層表達式,使用中間層表達式來進行優(yōu)化踊东,再把這種中間層表達式轉換成本地代碼北滥。
Oracle的HotSpot虛擬機使用了一種叫做 熱點編譯器(HotSpot Compiler)的JIT編譯器,之所以這么取名是因為是熱點編譯器會通過分析找到需要編譯的 "熱點" 代碼闸翅,然后將代碼編譯成本地代碼再芋。如果方法編譯后的字節(jié)碼不再被頻繁執(zhí)行,換句話說坚冀,如果這個方法不再是熱點济赎,HotSpot虛擬機會將這個方法對應的本地代碼從緩存中移除,并且會用解釋器模式來執(zhí)行记某。HotSpot虛擬機分成了Server VM 和 Client VM司训,這兩部分使用不同的JIT編譯器。
圖9: HotSpot VM的Client VM和Server VM
Client VM和Server VM使用相同的運行時液南,不過如上圖所示壳猜,它們的JIT編譯器是不同的。Server VM使用了更高級的動態(tài)優(yōu)化編譯器滑凉,這個編譯器使用很多復雜的性能優(yōu)化技術统扳。
IBM JVM自IBM JDK 6起引入了一種叫AOT(Ahead-Of-Time)的編譯器作為JIT編譯器,這意味著許多JVM通過共享緩存來共享編譯過的本地代碼畅姊,簡單來說咒钟,就是其他JVM可以直接使用AOT編譯器編譯過的代碼,而不用重新編譯若未。另外朱嘴,IBM JVM通過使用AOT編譯器將代碼預編譯成一種JXE(Java EXecutable)文件格式來提供一種更快速的執(zhí)行方式。
大多數Java性能的提高是通過優(yōu)化執(zhí)行引擎來完成的粗合,正如JIT編譯器中萍嬉,很多JVM性能的持續(xù)提高都是通過就、引各種優(yōu)化技術來完成舌劳。早期JVM和最近的JVM之間最大的不同就是執(zhí)行引擎的差異帚湘。
Oracle HotSpot JVM自1.3版本開始引入了熱點編譯器,Davlvik VM自android 2.2開始也引入了JIT編譯器甚淡。
譯者注:已省略翻譯最后的 JVM7規(guī)范以及結語部分大诸。