Java虛擬機之所以被稱之為是“虛擬”的蟀瞧,就是因為它僅僅是由一個規(guī)范來定義的抽象計算機看成。
3.1 Java虛擬機是什么
要理解Java虛擬機缭付,首先要意識到干毅,當你說“Java虛擬機”時,可能指的是如下三種不同的東西:
- 抽象規(guī)范
- 一個具體的實現(xiàn)
- 一個運行中的虛擬機實例
3.2 Java虛擬機的生命周期
一個運行時的Java虛擬機實例的天職是:負責運行一個Java程序涂圆。當啟動一個Java程序時们镜,一個虛擬機實例也就誕生了。當該程序關閉退出润歉,這個虛擬機實例也就隨之消亡模狭。如果在同一臺計算機上同時運行三個Java程序,將得到三個Java虛擬機實例踩衩。每個Java程序都運行于它自己的Java虛擬機實例中嚼鹉。
在Java虛擬機內(nèi)部有兩種線程:守護線程與非守護線程。守護線程通常是由虛擬機自己使用的驱富,比如執(zhí)行垃圾收集任務的線程锚赤。但是,Java程序也可以把它創(chuàng)建的任何線程標記為守護線程褐鸥。而Java程序中的初始線程——就是開始于main( )的那個宴树,是非守護線程。
只要還有任何非守護線程在運行晶疼,那么這個Java程序也在繼續(xù)運行(虛擬機仍然存活)。當該程序中所有的非守護線程都終止時又憨,虛擬機實例將自動退出翠霍。假若安全管理器允許,程序本身也能夠調(diào)用Runtime類或者System類的exit( )方法來退出蠢莺。
3.3 Java虛擬機的體系結構
當Java虛擬機運行一個程序時寒匙,它需要內(nèi)存來存儲許多東西,例如躏将,字節(jié)碼锄弱,從已裝載的class文件中得到的其他信息,程序創(chuàng)建的對象祸憋,傳遞給方法的參數(shù)会宪,返回值,局部變量蚯窥,以及運算的中間結果等掸鹅。Java虛擬機把這些東西都組織到幾個“運行時數(shù)據(jù)區(qū)”中塞帐,以便于管理。
每個Java虛擬機實例都有一個方法區(qū)以及一個堆巍沙,它們是由該虛擬機實例中所有線程共享的葵姥。
當每個新線程被創(chuàng)建時,它都將得到它自己的PC寄存器(程序計數(shù)器)以及一個Java棧句携。Java棧是由許多棧幀(stack frame)或者說幀(frame)組成的榔幸,一個棧幀包含一個Java方法調(diào)用時的狀態(tài)。
Java虛擬機沒有寄存器矮嫉,其指令集使用Java棧來存儲中間數(shù)據(jù)削咆。
數(shù)據(jù)類型
Java虛擬機的數(shù)據(jù)類型可以分為兩種:基本類型和引用類型。
Java語言中的所有基本類型同樣也都是Java虛擬機中的基本類型敞临。但是boolean有點特別态辛,雖然Java虛擬機也把boolean看做基本類型,但是指令集對boolean只有很有限的支持挺尿。當編譯器把Java源碼編譯為字節(jié)碼時奏黑,它會用int或byte來表示boolean。
Java虛擬機中還有一個只在內(nèi)部使用的基本類型:returnAddress编矾,Java程序員不能使用這個類型熟史。這個基本類型被用來實現(xiàn)Java程序中的finally字句。
字長的考量
在運行時窄俏,Java程序無法偵測底層虛擬機的字長大絮迤ァ;同樣凹蜈,虛擬機的字長大小也不會影響程序的行為——它僅僅是虛擬機實現(xiàn)的內(nèi)部屬性限寞。
類裝載器子系統(tǒng)
Java虛擬機有兩種類裝載器:啟動類裝載器和用戶自定義類裝載器。對于每一個被裝載的類型仰坦,Java虛擬機都會為它創(chuàng)建一個java.lang.Class
類的實例來代表該類型履植。和所有其他對象一樣,用戶自定義的類裝載器以及Class類的實例都放在內(nèi)存中的堆區(qū)悄晃,而裝載的類型信息則都位于方法區(qū)玫霎。
裝載,連接以及初始化妈橄。類裝載器子系統(tǒng)除了要定位和導入二進制class文件外庶近,還必須負責驗證被導入類的正確性,為類變量分配并初始化內(nèi)存眷蚓,以及幫助解析符號引用鼻种。這些動作必須嚴格按以下順序進行:
- 裝載——查找并裝載類型的二進制數(shù)據(jù)。
- 連接——執(zhí)行驗證沙热,準備普舆,以及解析(可選)恬口。
- 初始化——把類變量初始化為正確初始值。
用戶自定義類裝載器沼侣。類ClassLoader中的四個方法是通往Java虛擬機的通道:
protected final Class defineClass(String name, byte data[], int offset, int length);
protected final Class defineClass(String name, byte data[], int offset, int length, ProtectionDomain protectionDomain);
protected final Class findSystemClass(String name);
protected final void resolveClass(Class c);
任何Java虛擬機實現(xiàn)都必須把這些方法連到內(nèi)部的類裝載器子系統(tǒng)中祖能。每個Java虛擬機實現(xiàn)都必須保證
- ClassLoader類的defineClass( )方法能夠把新類型導入到方法區(qū)中。
- findSystemClass( )方法使用系統(tǒng)類裝載器(版本1.2以上)裝載指定類型蛾洛。
- resolveClass( )方法能夠?qū)︻愌b載器子系統(tǒng)執(zhí)行連接動作养铸。
命名空間。每個類裝載器都有自己的命名空間轧膘,其中維護著由它裝載的類型钞螟。Java虛擬機中的命名空間,其實是解析過程的結果谎碍。
方法區(qū)
當虛擬機運行Java程序時鳞滨,它會查找使用存儲在方法區(qū)中的類型信息。設計者應當為類型信息的內(nèi)部表示設計適當?shù)臄?shù)據(jù)結構蟆淀,以盡可能在保持虛擬機小巧緊湊的同時加快程序的運行效率拯啦。
由于所有線程都共享方法區(qū),因此它們對方法區(qū)數(shù)據(jù)的訪問必須被設計為是線程安全的熔任。
方法區(qū)的大小不必是固定的褒链,虛擬機可以根據(jù)應用的需要動態(tài)調(diào)整。同樣疑苔,方法區(qū)也不必是連續(xù)的甫匹,方法區(qū)可以在一個堆(甚至是虛擬機自己的堆)中自由分配。另外惦费,虛擬機也可以允許用戶或者程序員指定方法區(qū)的初始大小以及最小和最大尺寸等兵迅。
方法區(qū)也可以被垃圾收集。當某個類變?yōu)椴辉俦灰玫念悤r薪贫,Java虛擬機可以卸載這個類(垃圾收集)喷兼,從而使方法區(qū)占據(jù)的內(nèi)存保持最小。
類型信息后雷。對于每個被裝載的類型,虛擬機都會在方法區(qū)中存儲以下類型信息:
- 這個類型的全限定名
- 這個類型的直接超類的全限定名(除非這個類型是
java.lang.Object
吠各,它沒有超類) - 這個類型是類類型還是接口類型
- 這個類型的訪問修飾符(public臀突、abstract或final的某個子集)
- 任何直接超接口的全限定名的有序列表
在Java源代碼中,全限定名由類所屬包的名稱加上一個“.”贾漏,再加上類名組成候学。例如,類Object的所屬包為java.lang
纵散,那么它的全限定名應該是java.lang.Object
梳码,但在class文件里隐圾,所有的“.”都被斜杠“/”代替,這樣就成為java/lang/Object
掰茶。至于全限定名在方法區(qū)中的表示暇藏,則因不同的設計者決定,可以用任何形式和數(shù)據(jù)結構來代表濒蒋。
除了上面列出的基本類型信息外盐碱,虛擬機還得為每個被裝載的類型存儲以下信息:
- 該類型的常量池
- 字段信息
- 方法信息
- 除了常量以外的所有類(靜態(tài))變量
- 一個到類ClassLoader的引用
- 一個到Class類的引用
給定一個指向Class對象的引用,就可以通過Class類中定義的方法來找出這個類型的相關信息沪伙。Class類使得運行程序可以訪問方法區(qū)中保存的信息:
public String getName();
public Class getSuperClass();
public boolean isInterface();
public Class[] getInterfaces();
public ClassLoader getClassLoader();
堆
一個Java虛擬機實例中只存在一個堆空間瓮顽,因此所有線程都將共享這個堆。又由于一個Java程序獨占一個Java虛擬機實例围橡,因而每個Java程序都有它自己的堆空間——它們不會彼此干擾暖混。
Java虛擬機有一條在堆中分配新對象的指令,卻沒有釋放內(nèi)存的指令翁授。虛擬機自己負責決定如何以及何時釋放不再被運行的程序引用的對象所占據(jù)的內(nèi)存拣播。
和方法區(qū)一樣,堆空間也不必是連續(xù)的內(nèi)存區(qū)黔漂。在程序運行時诫尽,它可以動態(tài)擴展或收縮。某些實現(xiàn)可能也允許用戶或程序員指定堆的初始大小炬守、最大最小值等牧嫉。
Java對象中包含的基本數(shù)據(jù)由它所屬的類及其所有超類聲明的實例變量組成。只要有一個對象引用减途,虛擬機就必須能夠快速地定位對象實例的數(shù)據(jù)酣藻。另外,它也必須能通過該對象引用訪問相應的類數(shù)據(jù)(存儲于方法區(qū)的類型信息)鳍置。因此在對象中通常會有一個指向方法區(qū)的指針辽剧。
有如下幾個理由要求虛擬機必須能夠通過對象引用得到類(類型)數(shù)據(jù):
- 當程序在運行時需要轉(zhuǎn)換某個對象引用為另一種類型時,虛擬機必須要檢查這種轉(zhuǎn)換是否被允許税产。當程序在執(zhí)行instanceof操作時怕轿,虛擬機也進行了同樣的檢查。
- 當程序調(diào)用某個實例方法時辟拷,虛擬機必須進行動態(tài)綁定撞羽。
在Java中,數(shù)組是真正的對象衫冻。和其它對象一樣诀紊,數(shù)組總是存儲在堆中。同樣隅俘,和普通對象一樣邻奠,實現(xiàn)的設計者將決定數(shù)組在堆中的表示形式笤喳。
程序計數(shù)器
對于一個運行中的Java程序而言,其中的每一個線程都有它自己的PC(程序計數(shù)器)寄存器碌宴,它是在該線程啟動時創(chuàng)建的杀狡。當線程執(zhí)行某個Java方法時,PC寄存器的內(nèi)容總是下一條將被執(zhí)行指令的地址唧喉。如果該線程正在執(zhí)行一個本地方法捣卤,那么此時PC寄存器的值是“undefined”。
Java棧
每當啟動一個新線程時八孝,Java虛擬機都會為它分配一個Java棧董朝。Java棧以幀為單位保存線程的運行狀態(tài)。虛擬機只會直接對Java棧執(zhí)行兩種操作:以幀為單位的壓椄甚耍或出棧子姜。
Java棧上的所有數(shù)據(jù)都是此線程私有的。任何線程都不能訪問另一個線程的棧數(shù)據(jù)楼入。當一個線程調(diào)用一個方法時哥捕,方法的局部變量保存在調(diào)用線程Java棧的幀中。只有一個線程能總是訪問那些局部變量嘉熊,即調(diào)用方法的線程遥赚。
棧幀
棧幀由三部分組成:局部變量區(qū)、操作數(shù)棧和幀數(shù)據(jù)區(qū)阐肤。
局部變量區(qū)
class Example3a {
public static int runClassMethod(int i, long l, float f,
double d, Object o, byte b) {
return 0;
}
public int runInstanceMethod(char c, double d, short s,
boolean b) {
return 0;
}
}
類方法只與類相關凫佛,而與具體的對象無關。不能直接通過類方法訪問類實例的變量孕惜,因為在方法調(diào)用的時候沒有聯(lián)系到一個具體實例愧薛。
在源代碼中的byte、short衫画、char和boolean在局部變量區(qū)都被轉(zhuǎn)換成了int毫炉,在操作數(shù)棧中也一樣。前面提到過削罩,虛擬機并不直接支持boolean類型瞄勾,因此Java編譯器總是用int來表示boolean。但Java虛擬機對byte弥激、short和char是直接支持的进陡,這些類型的值可以作為實例變量或者數(shù)組元素存儲在局部變量區(qū),也可以作為類變量存儲在方法區(qū)中秆撮。但在局部變量區(qū)和操作數(shù)棧中都會被轉(zhuǎn)換成int類型的值。它們在棧幀中的時候都是當做int來進行處理的换况,只有當它被存回堆或方法區(qū)時职辨,才會轉(zhuǎn)換回原來的類型盗蟆。
在Java中,所有的對象都按引用傳遞舒裤,并且都存儲在堆中喳资,永遠都不會在局部變量區(qū)或操作數(shù)棧中發(fā)現(xiàn)對象的拷貝,只會有對象引用腾供。
操作數(shù)棧
和局部變量區(qū)一樣仆邓,操作數(shù)棧也是被組織成一個以字長為單位的數(shù)組。但是和前者不同的是伴鳖,它不是通過索引來訪問节值,而是通過標準的棧操作——壓棧和出棧——來訪問的榜聂。比如搞疗,如果某個指令把一個值壓入操作數(shù)棧中,稍后另一個指令就可以彈出這個值來使用须肆。
幀數(shù)據(jù)區(qū)
除了局部變量區(qū)和操作數(shù)棧外匿乃,Java棧幀還需要一些數(shù)據(jù)來支持常量池解析、正常方法返回以及異常派發(fā)機制豌汇。這些信息都保存在Java棧幀的幀數(shù)據(jù)區(qū)中幢炸。
本地方法棧
當線程調(diào)用本地方法時,虛擬機會保持Java棧不變拒贱,不再在線程的Java棧中壓入新的幀宛徊,虛擬機只是簡單地動態(tài)連接并直接調(diào)用指定的本地方法」袼迹可以把這看做是虛擬機利用本地方法來動態(tài)擴展自己岩调。
執(zhí)行引擎
任何Java虛擬機實現(xiàn)的核心都是它的執(zhí)行引擎。在Java虛擬機規(guī)范中赡盘,執(zhí)行引擎的行為使用指令集來定義号枕。
和開頭提到的對“Java虛擬機”這個術語有三種不同的理解一樣,“執(zhí)行引擎”這個術語也可以有三種理解:一個抽象的規(guī)范陨享,一個具體的實現(xiàn)葱淳,或是一個正在運行的實例。抽象規(guī)范使用指令集規(guī)定了執(zhí)行引擎的行為抛姑。具體實現(xiàn)可能使用多種不同的技術——包括軟件方面赞厕、硬件方面或數(shù)種技術的集合。作為運行時實例的執(zhí)行引擎就是一個線程定硝。
Java虛擬機指令集關注的中心是操作數(shù)棧皿桑。