在這篇文件里,我會(huì)闡述JVM是如何運(yùn)行的,包括它的結(jié)構(gòu),它如何去執(zhí)行字節(jié)碼,以及按照怎樣的順序去執(zhí)行,同時(shí)我還會(huì)給出一些常見錯(cuò)誤的示例以及對(duì)應(yīng)的解決辦法。
虛擬機(jī)(Virtual Machine)
JRE是由java API和JVM組成的。JVM的主要作用是通過Class Loader來加載Java程序,并且按照J(rèn)ava API來執(zhí)行加載的程序。
虛擬機(jī)是通過軟件的方式來模擬實(shí)現(xiàn)的機(jī)器(比如說計(jì)算機(jī)),它可以像物理機(jī)一樣運(yùn)行程序。設(shè)計(jì)虛擬機(jī)的初衷是讓Java能夠通過它來實(shí)現(xiàn)WORA(Write Once Run Anywher 一次編譯,到處運(yùn)行),盡管這個(gè)目標(biāo)現(xiàn)在已經(jīng)被大多數(shù)人忽略了。因此,JVM可以在不修改Java代碼的情況下,在所有的硬件環(huán)境上運(yùn)行Java字節(jié)碼。
Java虛擬機(jī)的特點(diǎn)如下:
基于棧的虛擬機(jī):Intel x86和ARM這兩種最常見的計(jì)算機(jī)體系的機(jī)構(gòu)都是基于寄存器的。不同的是,JVM是基于棧的。符號(hào)引用:除了基本類型以外的數(shù)據(jù)(類和接口)都是通過符號(hào)來引用,而不是通過顯式地使用內(nèi)存地址來引用。垃圾回收機(jī)制:類的實(shí)例都是通過用戶代碼進(jìn)行創(chuàng)建,并且自動(dòng)被垃圾回收機(jī)制進(jìn)行回收。通過對(duì)基本類型的清晰定義來保證平臺(tái)獨(dú)立性:傳統(tǒng)的編程語言,例如C/C++,int類型的大小取決于不同的平臺(tái)。JVM通過對(duì)基本類型的清晰定義來保證它的兼容性以及平臺(tái)獨(dú)立性。網(wǎng)絡(luò)字節(jié)碼順序:Java class文件用網(wǎng)絡(luò)字節(jié)碼順序來進(jìn)行存儲(chǔ):為了保證和小端的Intel x86架構(gòu)以及大端的RISC系列的架構(gòu)保持無關(guān)性,JVM使用用于網(wǎng)絡(luò)傳輸?shù)木W(wǎng)絡(luò)字節(jié)順序,也就是大端。雖然是Sun公司開發(fā)了Java,但是所有的開發(fā)商都可以開發(fā)并且提供遵循Java虛擬機(jī)規(guī)范的JVM。正是由于這個(gè)原因,使得不同的Oracle HotSpot和IBM JVM等不同的JVM能夠并存。Google的Android系統(tǒng)里的Dalvik VM也是一種JVM,雖然它并不遵循Java虛擬機(jī)規(guī)范。和基于棧的Java虛擬機(jī)不同,Dalvik VM是基于寄存器的架構(gòu),因此它的Java字節(jié)碼也被轉(zhuǎn)化成基于寄存器的指令集。
為了保證WORA,JVM使用Java字節(jié)碼這種介于Java和機(jī)器語言之間的中間語言。字節(jié)碼是部署Java代碼的最小單位。
在解釋Java字節(jié)碼之前,我們先通過實(shí)例來簡(jiǎn)單了解它。這個(gè)案例是一個(gè)在開發(fā)環(huán)境出現(xiàn)的真實(shí)案例的總結(jié)。
一個(gè)一直運(yùn)行正常的應(yīng)用突然無法運(yùn)行了。在類庫被更新之后,返回下面的錯(cuò)誤。
[java] view plaincopy簡(jiǎn)而言之,之前沒有返回值的addUser()被改修改成返回一個(gè)User類的實(shí)例的方法。不過,應(yīng)用的代碼沒有做任何修改,因?yàn)樗鼪]有使用addUser()的返回值。咋一看,com.nhn.user.UserAdmin.addUser()方法似乎仍然存在,如果存在的話,那么怎么還會(huì)出現(xiàn)NoSuchMethodError的錯(cuò)誤呢?
上面問題的原因是在于應(yīng)用的代碼沒有用新的類庫來進(jìn)行編譯。換句話來說,應(yīng)用代碼似乎是調(diào)了正確的方法,只是沒有使用它的返回值而已。不管怎樣,編譯后的class文件表明了這個(gè)方法是有返回值的。你可以從下面的錯(cuò)誤信息里看到答案。
[java] view plaincopyNoSuchMethodError出現(xiàn)的原因是“com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V”方法找不到。注意一下”Ljava/lang/String;”和最后面的“V”。在Java字節(jié)碼的表達(dá)式里,”L<classname>;”表示的是類的實(shí)例。這里表示addUser()方法有一個(gè)java/lang/String的對(duì)象作為參數(shù)。在這個(gè)類庫里,參數(shù)沒有被改變,所以它是正常的。最后面的“V”表示這個(gè)方法的返回值。在Java字節(jié)碼的表達(dá)式里,”V”表示沒有返回值(Void)。綜上所述,上面的錯(cuò)誤信息是表示有一個(gè)java.lang.String類型的參數(shù),并且沒有返回值的com.nhn.user.UserAdmin.addUser方法沒有找到。
因?yàn)閼?yīng)用是用之前的類庫編譯的,所以返回值為空的方法被調(diào)用了。但是在修改后的類庫里,返回值為空的方法不存在,并且添加了一個(gè)返回值為“Lcom/nhn/user/User”的方法。因此,就出現(xiàn)了NoSuchMethodError。
注:這個(gè)錯(cuò)誤出現(xiàn)的原因是因?yàn)殚_發(fā)者沒有用新的類庫來重新編譯應(yīng)用。不過,出現(xiàn)這種問題的大部分責(zé)任在于類庫的提供者。這個(gè)public的方法本來沒有返回值的,但是后來卻被修改成返回User類的實(shí)例。很明顯,方法的簽名被修改了,這也表明了這個(gè)類庫的后向兼容性被破壞了。因此,這個(gè)類庫的提供者應(yīng)該告知使用者這個(gè)方法已經(jīng)被改變了。
我們?cè)倩氐絁ava字節(jié)碼上來。Java字節(jié)碼是JVM很重要的部分。JVM是模擬執(zhí)行Java字節(jié)碼的一個(gè)模擬器。Java編譯器不會(huì)直接把高級(jí)語言(例如C/C++)編寫的代碼直接轉(zhuǎn)換成機(jī)器語言(CPU指令);它會(huì)把開發(fā)者可以理解的Java語言轉(zhuǎn)換成JVM能夠理解的Java字節(jié)碼。因?yàn)镴ava字節(jié)碼本身是平臺(tái)無關(guān)的,所以它可以在任何安裝了JVM(確切地說,是相匹配的JRE)的硬件上執(zhí)行,即使是在CPU和OS都不相同的平臺(tái)上(在Windows PC上開發(fā)和編譯的字節(jié)碼可以不做任何修改就直接運(yùn)行在linux機(jī)器上)。編譯后的代碼的大小和源代碼大小基本一致,這樣就可以很容易地通過網(wǎng)絡(luò)來傳輸和執(zhí)行編譯后的代碼。
Java class文件是一種人很難去理解的二進(jìn)文件。為了便于理解它,JVM提供者提供了javap,反匯編器。使用javap產(chǎn)生的結(jié)果是Java匯編語言。在上面的例子中,下面的Java匯編代碼是通過javap -c對(duì)UserServiceadd()方法進(jìn)行反匯編得到的。
[java] view plaincopyinvokeinterface:調(diào)用一個(gè)接口方法在這段Java匯編代碼中,addUser()方法是在第四行的“5:invokevitual#23″進(jìn)行調(diào)用的。這表示對(duì)應(yīng)索引為23的方法會(huì)被調(diào)用。索引為23的方法的名稱已經(jīng)被javap給注解在旁邊了。invokevirtual是Java字節(jié)碼里調(diào)用方法的最基本的操作碼。在Java字節(jié)碼里,有四種操作碼可以用來調(diào)用一個(gè)方法,分別是:invokeinterface,invokespecial,invokestatic以及invokevirtual。操作碼的作用分別如下:
invokespecial: 調(diào)用一個(gè)初始化方法,私有方法或者父類的方法invokestatic:調(diào)用靜態(tài)方法invokevirtual:調(diào)用實(shí)例方法Java字節(jié)碼的指令集由操作碼和操作數(shù)組成。類似invokevirtual這樣的操作數(shù)需要2個(gè)字節(jié)的操作數(shù)。
用更新的類庫來編譯上面的應(yīng)用代碼,然后反編譯它,將會(huì)得到下面的結(jié)果。
[java] view plaincopy你會(huì)發(fā)現(xiàn),對(duì)應(yīng)索引為23的方法被替換成了一個(gè)返回值為”Lcom/nhn/user/User”的方法。在上面的反匯編代碼里,代碼前面的數(shù)字代碼什么呢?
它表示的是字節(jié)數(shù)。大概這就是為什么運(yùn)行在JVM上面的代碼成為Java“字節(jié)”碼的原因。簡(jiǎn)而言之,Java字節(jié)碼指令的操作碼,例如aload_0,getfield和invokevirtual等,都是用一個(gè)字節(jié)的數(shù)字來表示的(aload_0=0x2a,getfield=0xb4,invokevirtual=0xb6)。由此可知Java字節(jié)碼指令的操作碼最多有256個(gè)。
aload_0和aload_1這樣的指令不需要任何操作數(shù)。因此,aload_0指令的下一個(gè)字節(jié)是下一個(gè)指令的操作碼。不過,getfield和invokevirtual指令需要2字節(jié)的操作數(shù)。因此,getfiled的下一條指令是跳過兩個(gè)字節(jié),寫在第四個(gè)字節(jié)的位置上的。十六進(jìn)制編譯器里查看字節(jié)碼的結(jié)果如下所示。
[java] view plaincopy
下面的表格給出了字節(jié)碼表達(dá)式的幾個(gè)實(shí)例。
表二:Java字節(jié)碼表達(dá)式范例

在講解Java class文件格式之前,我們先看看一個(gè)在Java Web應(yīng)用中經(jīng)常出現(xiàn)的問題。
當(dāng)我們編寫完jsp代碼,并且在Tomcat運(yùn)行時(shí),Jsp代碼沒有正常運(yùn)行,而是出現(xiàn)了下面的錯(cuò)誤。
[java] view plaincopy在不同的Web服務(wù)器上,上面的錯(cuò)誤信息可能會(huì)有點(diǎn)不同,不過有有一點(diǎn)肯定是相同的,它出現(xiàn)的原因是65535字節(jié)的限制。這個(gè)65535字節(jié)的限制是JVM規(guī)范里的限制,它規(guī)定了一個(gè)方法的大小不能超過65535字節(jié)。
下面我會(huì)更加詳細(xì)地講解這個(gè)65535字節(jié)限制的意義以及它出現(xiàn)的原因。
Java字節(jié)碼里的分支和跳轉(zhuǎn)指令分別是”goto”和”jsr”。
[java] view plaincopy有了這兩個(gè)指令,索引超過65535的分支也是可用的。因此,Java方法的65535字節(jié)的限制就可以解除了。不過,由于Java class文件的更多的其他的限制,使得Java方法還是不能超過65535字節(jié)。
為了展示其他的限制,我會(huì)簡(jiǎn)單講解一下class 文件的格式。
Java class文件的大致結(jié)構(gòu)如下:
[java] view plaincopy通過這些數(shù)值,我們可以來看看class文件的格式。
magic:class文件最開始的四個(gè)字節(jié)是魔數(shù)。它的值是用來標(biāo)識(shí)Java class文件的。從上面的內(nèi)容里可以看出,魔數(shù) 的值是0xCAFEBABE。簡(jiǎn)而言之,只有一個(gè)文件的起始4字節(jié)是0xCAFEBABE的時(shí)候,它才會(huì)被當(dāng)作Java class文件來處理。 minor_version,major_version:接下來的四個(gè)字節(jié)表示的是class文件的版本。UserService.class文件里的是0x00000032,所以這個(gè)class文件的版本是50.0。JDK 1.6編譯的class文件的版本是50.0,JDK 1.5編譯出來的class文件的版本是49.0。JVM必須對(duì)低版本的class文件保持后向兼容性,也就是低版本的class文件可以運(yùn)行在高版本的JVM上。不過,反過來就不行了,當(dāng)一個(gè)高版本的class文件運(yùn)行在低版本的JVM上時(shí),會(huì)出現(xiàn)java.lang.UnsupportedClassVersionError的錯(cuò)誤。constant_pool_count,constant_pool[]:在版本號(hào)之后,存放的是類的常量池。這里保存的信息將會(huì)放入運(yùn)行時(shí)常量池(Runtime Constant Pool)中去,這個(gè)后面會(huì)講解的。在加載一個(gè)class文件的時(shí)候,JVM會(huì)把常量池里的信息存放在方法區(qū)的運(yùn)行時(shí)常量區(qū)里。UserService.class文件里的constant_pool_count的值是0x0028,這表示常量池里有39(40-1)個(gè)常量。 access_flags:這是表示一個(gè)類的描述符的標(biāo)志;換句話說,它表示一個(gè)類是public,final還是abstract以及是不是接口的標(biāo)志。 fields_count,fields[]:當(dāng)前類的成員變量的數(shù)量以及成員變量的信息。成員變量的信息包含變量名,類型,修飾符以及變量在constant_pool里的索引。 methods_count,methods[]:當(dāng)前類的方法數(shù)量以及方法的信息。方法的信息包含方法名,參數(shù)的數(shù)量和類型,返回值的類型,修飾符,以及方法在constant_pool里的索引,方法的可執(zhí)行代碼以及異常信息。 attributes_count,attributes[]:attribution_info結(jié)構(gòu)包含不同種類的屬性。field_info和method_info里都包含了attribute_info結(jié)構(gòu)。javap簡(jiǎn)要地給出了class文件的一個(gè)可讀形式。當(dāng)你用”java -verbose”命令來分析UserService.class時(shí),會(huì)輸出如下的內(nèi)容:
[java] view plaincopyjavap輸出的內(nèi)容太長(zhǎng),我這里只是提出了整個(gè)輸出的一部分。整個(gè)的輸出展示了constant_pool里的不同信息,以及方法的內(nèi)容。
關(guān)于方法的65565字節(jié)大小的限制是和method_info struct相關(guān)的。method_info結(jié)構(gòu)包含Code,LineNumberTable,以及LocalViriable attribute幾個(gè)屬性,這個(gè)在“javap -verbose”的輸出里可以看到。Code屬性里的LineNumberTable,LocalVariableTable以及exception_table的長(zhǎng)度都是用一個(gè)固定的2字節(jié)來表示的。因此,方法的大小是不能超過LineNumberTable,LocalVariableTable以及exception_table的長(zhǎng)度的,它們都是65535字節(jié)。
許多人都在抱怨方法的大小限制,而且在JVM規(guī)范里還說名了”這個(gè)長(zhǎng)度以后有可能會(huì)是可擴(kuò)展的“。不過,到現(xiàn)在為止,還沒有為這個(gè)限制做出任何動(dòng)作。從JVM規(guī)范里的把class文件里的內(nèi)容直接拷貝到方法區(qū)這個(gè)特點(diǎn)來看,要想在保持后向兼容性的同時(shí)來擴(kuò)展方法區(qū)的大小是非常困難的。
如果因?yàn)镴ava編譯器的錯(cuò)誤而導(dǎo)致class文件的錯(cuò)誤,會(huì)怎么樣呢?或者,因?yàn)榫W(wǎng)絡(luò)傳輸?shù)腻e(cuò)誤導(dǎo)致拷貝的class文件的損壞呢?
為了預(yù)防這種場(chǎng)景,Java的類裝載器通過一個(gè)嚴(yán)格而且慎密的過程來校驗(yàn)class文件。在JVM規(guī)范里詳細(xì)地講解了這方面的內(nèi)容。
注意
我們?cè)鯓幽軌蚺袛郕VM正確地執(zhí)行了class文件校驗(yàn)的所有過程呢?我們?cè)趺磥砼袛嗖煌峁┥痰牟煌琂VM實(shí)現(xiàn)是符合JVM規(guī)范的呢?為了能夠驗(yàn)證以上兩點(diǎn),Oracle提供了一個(gè)測(cè)試工具TCK(Technology Compatibility Kit)。這個(gè)TCK工具通過執(zhí)行成千上萬的測(cè)試用例來驗(yàn)證一個(gè)JVM是否符合規(guī)范,這些測(cè)試?yán)锩姘烁鞣N非法的class文件。只有通過了TCK的測(cè)試的JVM才能稱作JVM。
和TCK相似,有一個(gè)組織JCP(Java Community Process;http://jcp.org)負(fù)責(zé)Java規(guī)范以及新的Java技術(shù)規(guī)范。對(duì)于JCP而言,如果要完成一項(xiàng)Java規(guī)范請(qǐng)求(Java Specification Request, JSR)的話,需要具備規(guī)范文檔,可參考的實(shí)現(xiàn)以及通過TCK測(cè)試。任何人如果想使用一項(xiàng)申請(qǐng)JSR的新技術(shù)的話,他要么使用RI提供許可的實(shí)現(xiàn),要么自己實(shí)現(xiàn)一個(gè)并且保證通過TCK的測(cè)試。
Java編寫的代碼會(huì)按照下圖的流程來執(zhí)行

類裝載器裝載負(fù)責(zé)裝載編譯后的字節(jié)碼,并加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)(Runtime Data Area),然后執(zhí)行引擎執(zhí)行會(huì)執(zhí)行這些字節(jié)碼。
Java提供了動(dòng)態(tài)的裝載特性;它會(huì)在運(yùn)行時(shí)的第一次引用到一個(gè)class的時(shí)候?qū)λM(jìn)行裝載和鏈接,而不是在編譯期進(jìn)行。JVM的類裝載器負(fù)責(zé)動(dòng)態(tài)裝載。Java類裝載器有如下幾個(gè)特點(diǎn):
層級(jí)結(jié)構(gòu):Java里的類裝載器被組織成了有父子關(guān)系的層級(jí)結(jié)構(gòu)。Bootstrap類裝載器是所有裝載器的父親。代理模式:基于層級(jí)結(jié)構(gòu),類的裝載可以在裝載器之間進(jìn)行代理。當(dāng)裝載器裝載一個(gè)類時(shí),首先會(huì)檢查它是否在父裝載器中進(jìn)行裝載了。如果上層的裝載器已經(jīng)裝載了這個(gè)類,這個(gè)類會(huì)被直接使用。反之,類裝載器會(huì)請(qǐng)求裝載這個(gè)類。可見性限制:一個(gè)子裝載器可以查找父裝載器中的類,但是一個(gè)父裝載器不能查找子裝載器里的類。不允許卸載:類裝載器可以裝載一個(gè)類但是不可以卸載它,不過可以刪除當(dāng)前的類裝載器,然后創(chuàng)建一個(gè)新的類裝載器。每個(gè)類裝載器都有一個(gè)自己的命名空間用來保存已裝載的類。當(dāng)一個(gè)類裝載器裝載一個(gè)類時(shí),它會(huì)通過保存在命名空間里的類全局限定名(Fully Qualified Class Name)進(jìn)行搜索來檢測(cè)這個(gè)類是否已經(jīng)被加載了。如果兩個(gè)類的全局限定名是一樣的,但是如果命名空間不一樣的話,那么它們還是不同的類。不同的命名空間表示class被不同的類裝載器裝載。
下圖展示了類裝載器的代理模型。

當(dāng)一個(gè)類裝載器(class loader)被請(qǐng)求裝載類時(shí),它首先按照順序在上層裝載器、父裝載器以及自身的裝載器的緩存里檢查這個(gè)類是否已經(jīng)存在。簡(jiǎn)單來說,就是在緩存里查看這個(gè)類是否已經(jīng)被自己裝載過了,如果沒有的話,繼續(xù)查找父類的緩存,直到在bootstrap類裝載器里也沒有找到的話,它就會(huì)自己在文件系統(tǒng)里去查找并且加載這個(gè)類。
啟動(dòng)類加載器(Bootstrap class loader):這個(gè)類裝載器是在JVM啟動(dòng)的時(shí)候創(chuàng)建的。它負(fù)責(zé)裝載Java API,包含Object對(duì)象。和其他的類裝載器不同的地方在于這個(gè)裝載器是通過native code來實(shí)現(xiàn)的,而不是用Java代碼。擴(kuò)展類加載器(Extension class loader):它裝載除了基本的Java API以外的擴(kuò)展類。它也負(fù)責(zé)裝載其他的安全擴(kuò)展功能。 系統(tǒng)類加載器(System class loader):如果說bootstrap class loader和extension class loader負(fù)責(zé)加載的是JVM的組件,那么system class loader負(fù)責(zé)加載的是應(yīng)用程序類。它負(fù)責(zé)加載用戶在$CLASSPATH里指定的類。用戶自定義類加載器(User-defined class loader):這是應(yīng)用程序開發(fā)者用直接用代碼實(shí)現(xiàn)的類裝載器。類似于web應(yīng)用服務(wù)(WAS)之類的框架會(huì)用這種結(jié)構(gòu)來對(duì)Web應(yīng)用和企業(yè)級(jí)應(yīng)用進(jìn)行分離。換句話來說,類裝載器的代理模型可以用來保證不同應(yīng)用之間的相互獨(dú)立。WAS類裝載器使用這種層級(jí)結(jié)構(gòu),不同的WAS供應(yīng)商的裝載器結(jié)構(gòu)有稍許區(qū)別。
如果類裝載器查找到一個(gè)沒有裝載的類,它會(huì)按照下圖的流程來裝載和鏈接這個(gè)類:

每個(gè)階段的描述如下:
Loading: 類的信息從文件中獲取并且載入到JVM的內(nèi)存里。 Verifying:檢查讀入的結(jié)構(gòu)是否符合Java語言規(guī)范以及JVM規(guī)范的描述。這是類裝載中最復(fù)雜的過程,并且花費(fèi)的時(shí)間也是最長(zhǎng)的。并且JVM TCK工具的大部分場(chǎng)景的用例也用來測(cè)試在裝載錯(cuò)誤的類的時(shí)候是否會(huì)出現(xiàn)錯(cuò)誤。Preparing:分配一個(gè)結(jié)構(gòu)用來存儲(chǔ)類信息,這個(gè)結(jié)構(gòu)中包含了類中定義的成員變量,方法和接口的信息。Resolving:把這個(gè)類的常量池中的所有的符號(hào)引用改變成直接引用。Initializing:把類中的變量初始化成合適的值。執(zhí)行靜態(tài)初始化程序,把靜態(tài)變量初始化成指定的值。JVM規(guī)范定義了上面的幾個(gè)任務(wù),不過它允許具體執(zhí)行的時(shí)候能夠有些靈活的變動(dòng)。

運(yùn)行時(shí)數(shù)據(jù)區(qū)是在JVM運(yùn)行的時(shí)候操作所分配的內(nèi)存區(qū)。運(yùn)行時(shí)內(nèi)存區(qū)可以劃分為6個(gè)區(qū)域。在這6個(gè)區(qū)域中,一個(gè)PC Register,JVM stack 以及Native Method Statck都是按照線程創(chuàng)建的,Heap,Method Area以及Runtime Constant Pool都是被所有線程公用的。
PC寄存器(PC register):每個(gè)線程啟動(dòng)的時(shí)候,都會(huì)創(chuàng)建一個(gè)PC(Program Counter ,程序計(jì)數(shù)器)寄存器。PC寄存器里保存有當(dāng)前正在執(zhí)行的JVM指令的地址。JVM 堆棧(JVM stack):每個(gè)線程啟動(dòng)的時(shí)候,都會(huì)創(chuàng)建一個(gè)JVM堆棧。它是用來保存棧幀的。JVM只會(huì)在JVM堆棧上對(duì)棧幀進(jìn)行push和pop的操作。如果出現(xiàn)了異常,堆棧跟蹤信息的每一行都代表一個(gè)棧幀立的信息,這些信息它是通過類似于printStackTrace()這樣的方法來展示的。
棧幀(stack frame):每當(dāng)一個(gè)方法在JVM上執(zhí)行的時(shí)候,都會(huì)創(chuàng)建一個(gè)棧幀,并且會(huì)添加到當(dāng)前線程的JVM堆棧上。當(dāng)這個(gè)方法執(zhí)行結(jié)束的時(shí)候,這個(gè)棧幀就會(huì)被移除。每個(gè)棧幀里都包含有當(dāng)前正在執(zhí)行的方法所屬類的本地變量數(shù)組,操作數(shù)棧,以及運(yùn)行時(shí)常量池的引用。本地變量數(shù)組的和操作數(shù)棧的大小都是在編譯時(shí)確定的。因此,一個(gè)方法的棧幀的大小也是固定不變的。-局部變量數(shù)組(Local variable array):這個(gè)數(shù)組的索引從0開始。索引為0的變量表示這個(gè)方法所屬的類的實(shí)例。從1開始,首先存放的是傳給該方法的參數(shù),在參數(shù)后面保存的是方法的局部變量。- 操作數(shù)棧(Operand stack):方法實(shí)際運(yùn)行的工作空間。每個(gè)方法都在操作數(shù)棧和局部變量數(shù)組之間交換數(shù)據(jù),并且壓入或者彈出其他方法返回的結(jié)果。操作數(shù)棧所需的最大空間是在編譯期確定的。因此,操作數(shù)棧的大小也可以在編譯期間確定。 本地方法棧(Native method stack):供用非Java語言實(shí)現(xiàn)的本地方法的堆棧。換句話說,它是用來調(diào)用通過JNI(Java Native Interface Java本地接口)調(diào)用的C/C++代碼。根據(jù)具體的語言,一個(gè)C堆棧或者C++堆棧會(huì)被創(chuàng)建。 方法區(qū)(Method area):方法區(qū)是所有線程共享的,它是在JVM啟動(dòng)的時(shí)候創(chuàng)建的。它保存所有被JVM加載的類和接口的運(yùn)行時(shí)常量池,成員變量以及方法的信息,靜態(tài)變量以及方法的字節(jié)碼。JVM的提供者可以通過不同的方式來實(shí)現(xiàn)方法區(qū)。在Oracle 的HotSpot JVM里,方法區(qū)被稱為永久區(qū)或者永久代(PermGen)。是否對(duì)方法區(qū)進(jìn)行垃圾回收對(duì)JVM的實(shí)現(xiàn)是可選的。 運(yùn)行時(shí)常量池(Runtime constant pool):這個(gè)區(qū)域和class文件里的constant_pool是相對(duì)應(yīng)的。這個(gè)區(qū)域是包含在方法區(qū)里的,不過,對(duì)于JVM的操作而言,它是一個(gè)核心的角色。因此在JVM規(guī)范里特別提到了它的重要性。除了包含每個(gè)類和接口的常量,它也包含了所有方法和變量的引用。簡(jiǎn)而言之,當(dāng)一個(gè)方法或者變量被引用時(shí),JVM通過運(yùn)行時(shí)常量區(qū)來查找方法或者變量在內(nèi)存里的實(shí)際地址。堆(Heap):用來保存實(shí)例或者對(duì)象的空間,而且它是垃圾回收的主要目標(biāo)。當(dāng)討論類似于JVM性能之類的問題時(shí),它經(jīng)常會(huì)被提及。JVM提供者可以決定怎么來配置堆空間,以及不對(duì)它進(jìn)行垃圾回收。現(xiàn)在我們?cè)贂?huì)過頭來看看之前反匯編的字節(jié)碼
[java] view plaincopy把上面的反匯編代碼和我們平時(shí)所見的x86架構(gòu)的匯編代碼相比較,我們會(huì)發(fā)現(xiàn)這兩者的結(jié)構(gòu)有點(diǎn)相似,都使用了操作碼;不過,有一點(diǎn)不同的地方是Java字節(jié)碼并不會(huì)在操作數(shù)里寫入寄存器的名稱、內(nèi)存地址或者偏移量。之前已經(jīng)說過,JVM用的是棧,它不會(huì)使用寄存器。和使用寄存器的x86架構(gòu)不同,它自己負(fù)責(zé)內(nèi)存的管理。它用索引例如15和23來代替實(shí)際的內(nèi)存地址。15和23都是當(dāng)前類(這里是UserService類)的常量池里的索引。簡(jiǎn)而言之,JVM為每個(gè)類創(chuàng)建了一個(gè)常量池,并且這個(gè)常量池里保存了實(shí)際目標(biāo)的引用。
每行反匯編代碼的解釋如下:
aload_0:把局部變量數(shù)組中索引為#0的變量添加到操作數(shù)棧上。索引#0所表示的變量是this,即是當(dāng)前實(shí)例的引用。getfield #15:把當(dāng)前類的常量池里的索引為#15的變量添加到操作數(shù)棧。這里添加的是UserAdmin的admin成員變量。因?yàn)閍dmin變量是個(gè)類的實(shí)例,因此添加的是一個(gè)引用。aload_1:把局部變量數(shù)組里的索引為#1的變量添加到操作數(shù)棧。來自局部變量數(shù)組里的索引為1的變量是方法的一個(gè)參數(shù)。因此,在調(diào)用add()方法的時(shí)候,會(huì)把userName指向的String的引用添加到操作數(shù)棧上。invokevirtual #23:調(diào)用當(dāng)前類的常量池里的索引為#23的方法。這個(gè)時(shí)候,通過getfile和aload_1添加到操作數(shù)棧上的引用都被作為方法的參數(shù)。當(dāng)方法運(yùn)行完成并且返回時(shí),它的返回值會(huì)被添加到操作數(shù)棧上。 pop:把通過invokevirtual調(diào)用的方法的返回值從操作數(shù)棧里彈出來。你可以看到,在前面的例子里,用老的類庫編譯的那段代碼是沒有返回值的。簡(jiǎn)而言之,正因?yàn)橹暗拇a沒有返回值,所以沒必要吧把返回值從操作數(shù)棧上給彈出來。 return:結(jié)束當(dāng)前方法調(diào)用下圖可以幫助你更好地理解上面的內(nèi)容。

順便提一下,在這個(gè)方法里,局部變量數(shù)組沒有被修改。所以上圖只顯示了操作數(shù)棧的變化。不過,大部分的情況下,局部變量數(shù)組也是會(huì)改變的。局部變量數(shù)組和操作數(shù)棧之間的數(shù)據(jù)傳輸是使用通過大量的load指令(aload,iload)和store指令(astore,istore)來實(shí)現(xiàn)的。
在這個(gè)圖里,我們簡(jiǎn)單驗(yàn)證了運(yùn)行時(shí)常量池和JVM棧的描述。當(dāng)JVM運(yùn)行的時(shí)候,每個(gè)類的實(shí)例都會(huì)在堆上進(jìn)行分配,User,UserAdmin,UserService以及String等類的信息都會(huì)保存在方法區(qū)。
通過類裝載器裝載的,被分配到JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū)的字節(jié)碼會(huì)被執(zhí)行引擎執(zhí)行。執(zhí)行引擎以指令為單位讀取Java字節(jié)碼。它就像一個(gè)CPU一樣,一條一條地執(zhí)行機(jī)器指令。每個(gè)字節(jié)碼指令都由一個(gè)1字節(jié)的操作碼和附加的操作數(shù)組成。執(zhí)行引擎取得一個(gè)操作碼,然后根據(jù)操作數(shù)來執(zhí)行任務(wù),完成后就繼續(xù)執(zhí)行下一條操作碼。
不過Java字節(jié)碼是用一種人類可以讀懂的語言編寫的,而不是用機(jī)器可以直接執(zhí)行的語言。因此,執(zhí)行引擎必須把字節(jié)碼轉(zhuǎn)換成可以直接被JVM執(zhí)行的語言。字節(jié)碼可以通過以下兩種方式轉(zhuǎn)換成合適的語言。
解釋器:一條一條地讀取,解釋并且執(zhí)行字節(jié)碼指令。因?yàn)樗粭l一條地解釋和執(zhí)行指令,所以它可以很快地解釋字節(jié)碼,但是執(zhí)行起來會(huì)比較慢。這是解釋執(zhí)行的語言的一個(gè)缺點(diǎn)。字節(jié)碼這種“語言”基本來說是解釋執(zhí)行的。即時(shí)(Just-In-Time)編譯器:即時(shí)編譯器被引入用來彌補(bǔ)解釋器的缺點(diǎn)。執(zhí)行引擎首先按照解釋執(zhí)行的方式來執(zhí)行,然后在合適的時(shí)候,即時(shí)編譯器把整段字節(jié)碼編譯成本地代碼。然后,執(zhí)行引擎就沒有必要再去解釋執(zhí)行方法了,它可以直接通過本地代碼去執(zhí)行它。執(zhí)行本地代碼比一條一條進(jìn)行解釋執(zhí)行的速度快很多。編譯后的代碼可以執(zhí)行的很快,因?yàn)楸镜卮a是保存在緩存里的。不過,用JIT編譯器來編譯代碼所花的時(shí)間要比用解釋器去一條條解釋執(zhí)行花的時(shí)間要多。因此,如果代碼只被執(zhí)行一次的話,那么最好還是解釋執(zhí)行而不是編譯后再執(zhí)行。因此,內(nèi)置了JIT編譯器的JVM都會(huì)檢查方法的執(zhí)行頻率,如果一個(gè)方法的執(zhí)行頻率超過一個(gè)特定的值的話,那么這個(gè)方法就會(huì)被編譯成本地代碼。

JVM規(guī)范沒有定義執(zhí)行引擎該如何去執(zhí)行。因此,JVM的提供者通過使用不同的技術(shù)以及不同類型的JIT編譯器來提高執(zhí)行引擎的效率。
大部分的JIT編譯器都是按照下圖的方式來執(zhí)行的:

JIT編譯器把字節(jié)碼轉(zhuǎn)換成一個(gè)中間層表達(dá)式,一種中間層的表示方式,來進(jìn)行優(yōu)化,然后再把這種表示轉(zhuǎn)換成本地代碼。
Oracle Hotspot VM使用一種叫做熱點(diǎn)編譯器的JIT編譯器。它之所以被稱作”熱點(diǎn)“是因?yàn)闊狳c(diǎn)編譯器通過分析找到最需要編譯的“熱點(diǎn)”代碼,然后把熱點(diǎn)代碼編譯成本地代碼。如果已經(jīng)被編譯成本地代碼的字節(jié)碼不再被頻繁調(diào)用了,換句話說,這個(gè)方法不再是熱點(diǎn)了,那么Hotspot VM會(huì)把編譯過的本地代碼從cache里移除,并且重新按照解釋的方式來執(zhí)行它。Hotspot VM分為Server VM和Client VM兩種,這兩種VM使用不同的JIT編譯器。

Client VM 和Server VM使用完全相同的運(yùn)行時(shí),不過如上圖所示,它們所使用的JIT編譯器是不同的。Server VM用的是更高級(jí)的動(dòng)態(tài)優(yōu)化編譯器,這個(gè)編譯器使用了更加復(fù)雜并且更多種類的性能優(yōu)化技術(shù)。
IBM 在IBM JDK 6里不僅引入了JIT編譯器,它同時(shí)還引入了AOT(Ahead-Of-Time)編譯器。它使得多個(gè)JVM可以通過共享緩存來共享編譯過的本地代碼。簡(jiǎn)而言之,通過AOT編譯器編譯過的代碼可以直接被其他JVM使用。除此之外,IBM JVM通過使用AOT編譯器來提前把代碼編譯器成JXE(Java EXecutable)文件格式來提供一種更加快速的執(zhí)行方式。
大部分Java程序的性能都是通過提升執(zhí)行引擎的性能來達(dá)到的。正如JIT編譯器一樣,很多優(yōu)化的技術(shù)都被引入進(jìn)來使得JVM的性能一直能夠得到提升。最原始的JVM和最新的JVM最大的差別之處就是在于執(zhí)行引擎。
Hotspot編譯器在1.3版本的時(shí)候就被引入到Oracle Hotspot VM里了,JIT編譯技術(shù)在Anroid 2.2版本的時(shí)候被引入到Dalvik VM里。
引入一種中間語言,例如字節(jié)碼,虛擬機(jī)執(zhí)行字節(jié)碼,并且通過JIT編譯器來提升JVM的性能的這種技術(shù)以及廣泛應(yīng)用在使用中間語言的編程語言上。例如微軟的.Net,CLR(Common Language Runtime 公共語言運(yùn)行時(shí)),也是一種VM,它執(zhí)行一種被稱作CIL(Common Intermediate Language)的字節(jié)碼。CLR提供了AOT編譯器和JIT編譯器。因此,用C#或者VB.NET編寫的源代碼被編譯后,編譯器會(huì)生成CIL并且CIL會(huì)執(zhí)行在有JIT編譯器的CLR上。CLR和JVM相似,它也有垃圾回收機(jī)制,并且也是基于堆棧運(yùn)行。
2011年7月28日,Oracle發(fā)布了Java SE的第7個(gè)版本,并且把JVM規(guī)也更新到了相應(yīng)的版本。在1999年發(fā)布《The Java Virtual Machine Specification,Second Edition》后,Oracle花了12年來發(fā)布這個(gè)更新的版本。這個(gè)更新的版本包含了這12年來累積的眾多變化以及修改,并且更加細(xì)致地對(duì)規(guī)范進(jìn)行了描述。此外,它還反映了《The Java Language Specificaion,Java SE 7 Edition》里的內(nèi)容。主要的變化總結(jié)如下:
來自Java SE 5.0里的泛型,支持可變參數(shù)的方法從Java SE 6以來,字節(jié)碼校驗(yàn)的處理技術(shù)所發(fā)生的改變添加invokedynamic指令以及class文件對(duì)于該指令的支持刪除了關(guān)于Java語言概念的內(nèi)容,并且指引讀者去參考Java語言規(guī)范 刪除關(guān)于Java線程和鎖的描述,并且把它們移到Java語言規(guī)范里最大的改變是添加了invokedynamic指令。也就是說JVM的內(nèi)部指令集做了修改,使得JVM開始支持動(dòng)態(tài)類型的語言,這種語言的類型不是固定的,例如腳本語言以及來自Java SE 7里的Java語言。之前沒有被用到的操作碼186被分配給新指令invokedynamic,而且class文件格式里也添加了新的內(nèi)容來支持invokedynamic指令。
Java SE 7的編譯器生成的class文件的版本號(hào)是51.0。Java SE 6的是50.0。class文件的格式變動(dòng)比較大,因此,51.0版本的class文件不能夠在Java SE 6的虛擬機(jī)上執(zhí)行。
盡管有了這么多的變動(dòng),但是Java方法的65535字節(jié)的限制還是沒有被去掉。除非class文件的格式徹底改變,否者這個(gè)限制將來也是不可能去掉的。
值得說明的是,Oracle Java SE 7 VM支持G1這種新的垃圾回收機(jī)制,不過,它被限制在Oracle JVM上,因此,JVM本身對(duì)于垃圾回收的實(shí)現(xiàn)不做任何限制。也因此,在JVM規(guī)范里沒有對(duì)它進(jìn)行描述。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注