深入理解Java之java虛擬機(jī)干凈利落的規(guī)范總結(jié) 上
- 作者:新網(wǎng)
- 來源:新網(wǎng)
- 瀏覽:100
- 2018-05-03 17:57:51
要去正確地實(shí)現(xiàn)一臺Java虛擬機(jī),就需要正確地讀取class文件中每一條字節(jié)碼指令并且能正確執(zhí)行這些指令所蘊(yùn)含的操作即可。
<
div> 要去正確地實(shí)現(xiàn)一臺Java
虛擬機(jī),就需要正確地讀取class文件中每一條字節(jié)碼指令并且能正確執(zhí)行這些指令所蘊(yùn)含的操作即可。
數(shù)據(jù)類型
和Java語言類似,在Java虛擬機(jī)中的數(shù)據(jù)類型也可以分為基本類型和引用類型兩種,所以也存在原始值和引用值兩種類型的數(shù)值。它們可用于變量賦值、參數(shù)傳遞、方法返回和運(yùn)算操作。
原始類型與值
Java虛擬機(jī)所支持的原始數(shù)據(jù)類型包括數(shù)值類型、boolean類型、和returnAddress類型
數(shù)值類型分為整數(shù)類型和浮點(diǎn)類型,分別是char,byte,short,int,long;浮點(diǎn)類型即float和double,這里和Java語言中的一致。
returnAddress翻譯過來是返回地址,其實(shí)returnAddress類型的值指向一條虛擬機(jī)指令的操作碼。它在虛擬機(jī)中比較典型的一個應(yīng)用場景是用于jsr程序段落跳轉(zhuǎn),在try-catch異常處理以及finally代碼塊經(jīng)常出現(xiàn)。和數(shù)值類的原生類型不同,returnAddress類型在Java語言之中并不存在相應(yīng)的類型,而且也無法在程序運(yùn)行期間修改。
雖然Java虛擬機(jī)定義了boolean這種數(shù)據(jù)類型,但是只對它提供了十分有限的支持。在Java虛擬機(jī)中并沒有任何供boolean值專用的字節(jié)碼指令,Java語言表達(dá)式所操作的boolean值,在編譯之后都使用Java虛擬機(jī)中的int數(shù)據(jù)類型來代替。(Java虛擬機(jī)會把boolean數(shù)組元素中的true采用1來表示,false采用0來表示,當(dāng)Java編譯器把Java語言中的boolean類型值映射為Java虛擬機(jī)的int類型值時,也必須用上述表示方式)
引用類型與值
Java虛擬機(jī)中有三種引用類型:類類型、數(shù)組類型和接口類型。它們分別指向動態(tài)創(chuàng)建的類實(shí)例、數(shù)組實(shí)例和某個接口的類實(shí)例或數(shù)組實(shí)例。
數(shù)組類型最外面那一維元素的類型叫做數(shù)組類型的組件類型。一個數(shù)組的組件類型也可以是數(shù)組。從任意一個數(shù)組開始,如果發(fā)現(xiàn)其組件類型也是數(shù)組類型,那就繼續(xù)取這個小數(shù)組的組件類型,不斷執(zhí)行這樣的操作,最終一定可以遇到組件類型不是數(shù)組的情況,這時就把這種類型成為本數(shù)組的元素類型。數(shù)組的元素類型必須是原生類型、類類型或者接口類型之一。
在引用類型的值中還有一個特殊的值:null,當(dāng)一個引用不指向任何對象的時候,它的值就用null來表示。一個為null的引用,起初并不具備任何實(shí)際的運(yùn)行期類型,但是它可轉(zhuǎn)型為任意的引用類型。引用類型的默認(rèn)值就是null。Java虛擬機(jī)規(guī)范并沒有規(guī)定null在虛擬機(jī)實(shí)現(xiàn)中應(yīng)當(dāng)怎樣用編碼來表示。
運(yùn)行時數(shù)據(jù)區(qū)域
棧幀:棧幀是用來存儲數(shù)據(jù)和部分過程結(jié)果的數(shù)據(jù)結(jié)構(gòu),同時也用來處理動態(tài)鏈接、方法返回值和異常分派。棧幀又是存儲在棧中(包括Java虛擬機(jī)棧和本地方法棧),它隨著方法調(diào)用而創(chuàng)建,隨著方法結(jié)束而銷毀,其實(shí)也就是一個方法執(zhí)行的過程也對應(yīng)著棧幀的入棧和出棧的過程。無論方法是正常完成還是異常完成(拋出了在方法內(nèi)未被捕獲的異常)都算作方法結(jié)束。棧幀的存儲
空間由創(chuàng)建它的線程分配在Java虛擬機(jī)棧之中,每一個棧幀都有自己的本地變量表、操作數(shù)棧和指向當(dāng)前方法所屬的類的運(yùn)行時常量池的引用。
本地變量表和操作數(shù)棧的容量在編譯期確定,并通過相關(guān)方法的code屬性保存及提供給棧幀使用。因此,棧幀數(shù)據(jù)結(jié)構(gòu)的大小僅僅取決于Java虛擬機(jī)的實(shí)現(xiàn)。實(shí)現(xiàn)者可以在調(diào)用方法的時候給它們分配內(nèi)存。
在某條線程執(zhí)行的過程中的某個時間點(diǎn),只有目前正在執(zhí)行的那個方法的棧幀是活動的。這個棧幀稱為當(dāng)前棧幀,這個棧幀對應(yīng)的方法稱為當(dāng)前方法,定義這個方法的類稱作當(dāng)前類。對局部變量表和操作數(shù)棧的各種操作,通常都是值對當(dāng)前棧幀的局部變量表和操作數(shù)棧所進(jìn)行的操作。
如果當(dāng)前方法調(diào)用了其他方法,或者當(dāng)前方法執(zhí)行結(jié)束,那這個方法的棧幀就不再是當(dāng)前棧幀了。調(diào)用新方法時,新的棧幀也會隨之而創(chuàng)建,并且會隨著程序控制權(quán)移交到新方法而成為新的當(dāng)前棧幀。方法返回之際,當(dāng)前棧幀會傳回此方法給前一個棧幀,然后虛擬機(jī)會丟棄當(dāng)前棧幀,使得前一個棧幀重新成為當(dāng)前棧幀。
需要特別注意的是,棧幀是線程本地私有的數(shù)據(jù),不可能在一個棧幀之中引用另外一個線程的棧幀。
局部變量表
每個棧幀內(nèi)部都包含一組稱為局部變量表的變量列表。棧幀中局部變量表的長度由編譯器決定,并卻存儲于類或接口的二進(jìn)制表示之中,即通過方法的code屬性保存及提供給棧幀使用。
一個局部變量可以保存一個類型為boolean、byte、char、short、int、float、reference或returnAddress的數(shù)據(jù)。兩個局部變量可以保存一個類型為long或double的數(shù)據(jù)。
局部變量使用索引來進(jìn)行定位訪問。首個局部變量的索引值為0。局部變量的索引值是個整數(shù),它大于等于0,且小于局部變量表的長度。Java虛擬機(jī)使用局部變量表來完成方法調(diào)用時的參數(shù)傳遞。當(dāng)調(diào)用類方法時,它的參數(shù)將會依次傳遞到局部變量表中從0開始的連續(xù)位置上。當(dāng)調(diào)用實(shí)例方法時,第0個局部變量一定用來存儲該實(shí)例方法所在對象的引用(即Java語言中的this關(guān)鍵字)。后續(xù)其他參數(shù)將會傳遞至局部變量表中從1開始的連續(xù)位置上。
操作數(shù)棧:每個棧幀內(nèi)部都包含一個稱為操作數(shù)棧的后進(jìn)后出棧。棧幀中操作數(shù)棧的最大深度由編譯期決定,并且通過方法的code屬性保存及提供給棧幀使用。棧幀剛創(chuàng)建的時候操作數(shù)棧是空的。Java虛擬機(jī)提供一些字節(jié)碼指令來從局部變量表或者對象實(shí)例的字段中復(fù)制常量或變量值到操作數(shù)棧中,也提供了一些指令用于從操作數(shù)棧取走數(shù)據(jù)、操作數(shù)據(jù)以及把操作結(jié)果重新入棧。在調(diào)用方法時,操作數(shù)棧也用來準(zhǔn)備調(diào)用方法的參數(shù)以及接收方法返回結(jié)果。例如iadd字節(jié)碼指令的作用是將兩個int類型的數(shù)值相加,它要求在執(zhí)行之前操作數(shù)棧的棧頂已經(jīng)存在兩個由前面的其他指令所放入的int類型數(shù)值。在執(zhí)行iadd指令時,兩個int類型數(shù)值出棧,相加求和之后求和結(jié)果重新入棧。操作數(shù)棧的每個位置上可以保存一個Java虛擬機(jī)中定義的數(shù)據(jù)類型的值,包括long和double類型。在任意時刻,操作數(shù)棧都會有一個確定的棧深度,一個long或者double類型的數(shù)據(jù)會占用兩個單位的棧深度,其他數(shù)據(jù)類型則會占用一個單位的棧深度。
動態(tài)鏈接
每個棧幀內(nèi)部都包含一個指向當(dāng)前方法所在類型的運(yùn)行時常量池的引用,以便對當(dāng)前方法的代碼實(shí)現(xiàn)動態(tài)鏈接。在class文件里面一個方法若要調(diào)用其他方法,或者訪問成員變量,則需要通過符號引用來表示。動態(tài)鏈接的作用就是將這些符號引用所表示的方法轉(zhuǎn)換為對實(shí)際方法的直接引用。類加載的過程中將要解析尚未被解析的符號引用,并且將對變量的訪問轉(zhuǎn)化為變量在程序運(yùn)行時,位于存儲結(jié)構(gòu)中的正確偏移量。由于對其他類中的方法和變量進(jìn)行了晚期綁定,所以即便那些類發(fā)生變化,也不會影響調(diào)用它們的方法。
對象的表示
Java虛擬機(jī)規(guī)范不強(qiáng)制規(guī)定對象的內(nèi)部結(jié)構(gòu)應(yīng)該如何表示。在具體實(shí)現(xiàn)中,一般有兩種對象的訪問方式,分別是通過句柄訪問對象以及通過直接指針訪問對象。
在之前的博客里我也總結(jié)過這兩種對象訪問方式的優(yōu)劣,想要了解的同學(xué)可以參考這篇博客
特殊方法
在Java虛擬機(jī)層面,Java編程語言的構(gòu)造器是以一個名為的特殊實(shí)例初始方法的形式出現(xiàn)的。這個方法名稱是由編譯器命名的,因為它并非一個合法的Java方法名字,不可能通過程序編碼的方式實(shí)現(xiàn)。實(shí)例初始化方法的初始化期間,通過Java虛擬機(jī)的invokespecial指令來調(diào)用,而且只能在尚未初始化的實(shí)力上調(diào)用該指令。構(gòu)造器的訪問權(quán)限也會約束由該構(gòu)造器所衍生出來的實(shí)例初始化方法。
一個類或者接口最多可以包含不超過一個類或接口的初始化方法,類或接口就是通過這個方法完成初始化的。這個方法是一個不包含參數(shù)的、返回類型為void的方法,名為。
在class文件中把其他方法命名為是沒有意義的,這些方法并不是類或接口的初始化方法,它們既不能被字節(jié)碼指令調(diào)用,也不會被虛擬機(jī)自己調(diào)用。當(dāng)class文件的版本號不小于51.0時,方法想要成為類或接口的初始化方法,必須設(shè)置ACC_STATIC標(biāo)志。
異常
Java虛擬機(jī)里面的異常使用Throwable或其子類的實(shí)例來表示,拋異常的本質(zhì)實(shí)際上是程序控制權(quán)轉(zhuǎn)移的一種即時的、非局部的轉(zhuǎn)換---從異常拋出的地方轉(zhuǎn)換至異常處理的地方。
絕大多數(shù)異常的產(chǎn)生都是由于當(dāng)前線程執(zhí)行的某個操作所導(dǎo)致的,這種可以稱為同步異常。與之相對,異步異??梢栽诔绦驁?zhí)行過程中隨時發(fā)生。Java虛擬機(jī)中異常的出現(xiàn)總是由下面三種原因之一導(dǎo)致的:
athrow字節(jié)碼指令被執(zhí)行;
虛擬機(jī)同步檢測到程序發(fā)生了非正常的執(zhí)行情況,這時異常必將緊接著發(fā)生在非正常執(zhí)行情況的字節(jié)碼指令之后拋出,而不會在執(zhí)行程序的過程中隨時拋出。例如:程序所執(zhí)行的操作可能會引發(fā)異常---當(dāng)字節(jié)碼指令所蘊(yùn)含的操作違反了Java語言的語義,如訪問一個超出數(shù)組邊界范圍的元素,或者是當(dāng)程序在加載或者連接時出現(xiàn)錯誤;還有一種異常是使用某些資源的時候產(chǎn)生資源限制,比如說使用了太多的內(nèi)存。
由于以下原因,導(dǎo)致了異步異常的發(fā)生: 調(diào)用了Thread或者ThreadGroup的stop方法;Java虛擬機(jī)實(shí)現(xiàn)發(fā)生了內(nèi)部錯誤。
當(dāng)某個線程調(diào)用了stop方法時,將會影響到其他線程,或者在特定線程組中的所有線程。因為stop方法的執(zhí)行常常會導(dǎo)致出現(xiàn)數(shù)據(jù)不一致的情況,這時候其他線程中出現(xiàn)的異常就是異步異常,因為這些異??赡艹霈F(xiàn)在線程執(zhí)行過程中的任何位置。虛擬機(jī)的內(nèi)部錯誤也被認(rèn)為是一種異步異常。
虛擬機(jī)錯誤
InternalError:實(shí)現(xiàn)虛擬機(jī)的軟件錯誤、底層
主機(jī)系統(tǒng)的軟件錯誤及硬件錯誤都會導(dǎo)致Java虛擬機(jī)出現(xiàn)內(nèi)部錯誤,InternalError是一個異步異常,它可能出現(xiàn)在程序中的任何位置;
OutOfMemoryError:當(dāng)Java虛擬機(jī)實(shí)現(xiàn)耗盡了所有虛擬或物理內(nèi)存,并且內(nèi)存自動管理子系統(tǒng)無法回收到創(chuàng)建新對象所需的足夠內(nèi)存空間時,虛擬機(jī)將拋出OutOfMemoryError。
StackOverflowError:當(dāng)Java虛擬機(jī)實(shí)現(xiàn)耗盡了線程全部的??臻g時,虛擬機(jī)將會拋出StackOverflowError。
UnKnownError:當(dāng)某種錯誤或異常出現(xiàn),但虛擬機(jī)實(shí)現(xiàn)又無法確定它具體是哪種異常或錯誤,將會拋出UnKnownError。
由于通常虛擬機(jī)會對代碼進(jìn)行優(yōu)化,例如指令重排。那么在異常發(fā)生的時候,有一些在異常出現(xiàn)位置之后的代碼可能已經(jīng)執(zhí)行了,那這些優(yōu)化過的代碼必須保證它們提前執(zhí)行所產(chǎn)生的影響對用戶程序來說是不可見的。