Java 虛擬機器結構
一 資料型別
與 Java 程式語言中的資料型別相似,Java 虛擬機器可以操作的資料型別可分為兩類:原始型別(Primitive Types,也經常翻譯為原生型別或者基本型別)和引用型別(Reference Types)。 與之對應,也存在有原始值(Primitive Values)和引用值(Reference Values)兩種型別的數值可用於變數賦值、引數傳遞、方法返回和運算操作。
二 原始型別與值
Java 虛擬機器所支援的原始資料型別包括了數值型別(Numeric Types)、布林型別(Boolean Type)和 returnAddress 型別三類。其中數值型別又分為整型型別(Integral Types)和浮點型別(Floating-Point Types)兩種,
其中整數型別包括:
-
byte 型別:值為 8 位有符號二進位制補碼整數,預設值為零。
-
short 型別:值為 16 位有符號二進位制補碼整數,預設值為零。
-
int 型別:值為 32 位有符號二進位制補碼整數,預設值為零。
-
long 型別:值為 64 位有符號二進位制補碼整數,預設值為零。
-
char 型別:值為使用 16 位無符號整數表示的、指向基本多文字平面(Basic Multilingual Plane,BMP)的 Unicode 值,以 UTF-16 編碼,預設值為 Unicode 的 null 值('\u0000')。
浮點型別包括:
-
float 型別:值為單精度浮點數集合②中的元素,或者(如果虛擬機器支援的話)是單精度 擴充套件指數(Float-Extended-Exponent)集合中的元素。預設值為正數零。
-
double 型別:取值範圍是雙精度浮點數集合中的元素,或者(如果虛擬機器支援的話)是 雙精度擴充套件指數(Double-Extended-Exponent)集合中的元素。預設值為正數零。 布林型別:
-
boolean 型別:取值範圍為布林值 true 和 false,預設值為 false。 returnAddress 型別:
-
returnAddress 型別:表示一條位元組碼指令的操作碼(Opcode)。在所有的虛擬機器支 持的原始型別之中,只有 returnAddress 型別是不能直接 Java 語言的資料型別對應 起來的。
2.1 整型型別與整型值
Java 虛擬機器中的整型型別的取值範圍如下:
-
對於 byte 型別,取值範圍是從 -128 至 127(-27至 27-1),包括 -128 和 127。
-
對於 short 型別,取值範圍是從 −32768 至 32767(-215至 215-1),包括 −32768 和 32767。
-
對於 int 型別,取值範圍是從 −2147483648 至 2147483647(-231至 231-1),包括 −2147483648 和 2147483647。
-
對於 long 型別,取值範圍是從−9223372036854775808 至 9223372036854775807 (-263 至 263-1),包括 −9223372036854775808 和 9223372036854775807。
-
對於 char 型別,取值範圍是從 0 至 65535,包括 0 和 65535。
2.2 浮點型別、取值集合及浮點值
浮點型別包含 float 型別和 double 型別兩種,它們在概念上與《IEEE Standard for Binary Floating-Point Arithmetic》ANSI/IEEE Std. 754-1985(IEEE, New York) 標準中定義的 32 位單精度和 64 位雙精度 IEEE 754 格式取值和操作都是一致的。
IEEE 754 標準的內容不僅包括了正負帶符號可數的數值(Sign-Magnitude Numbers), 還包括了正負零、正負無窮大和一個特殊的“非數字”標識(Not-a-Number,下文用 NaN 表示)。 NaN 值用於表示某些無效的運算操作,例如除數為零等情況。
所有 Java 虛擬機器的實現都必須支援兩種標準的浮點數值集合:單精度浮點數集合和雙精度浮 點數集合。另外,Java 虛擬機器實現可以自由選擇是否要支援單精度擴充套件指數集合和雙精度擴充套件指 數集合,也可以選擇支援其中的一種或全部。這些擴充套件指數集合可能在某些特定情況下代替標準浮 點數集合來表示 float 和 double 型別的數值。
2.3 returnAddress 型別和值
returnAddress 型別會被 Java 虛擬機器的 jsr、ret 和 jsr_w 指令所使用。 returnAddress 型別的值指向一條虛擬機器指令的操作碼。與前面介紹的那些數值類的原始型別 不同,returnAddress 型別在 Java 語言之中並不存在相應的型別,也無法在程式執行期間更改 returnAddress 型別的值。
2.4 boolean 型別
雖然 Java 虛擬機器定義了 boolean 這種資料型別,但是隻對它提供了非常有限的支援。在 Java 虛擬機器中沒有任何供 boolean 值專用的位元組碼指令,在 Java 語言之中涉及到 boolean 型別值的運算,在編譯之後都使用 Java 虛擬機器中的 int 資料型別來代替。 Java 虛擬機器直接支援 boolean 型別的陣列,虛擬機器的 newarray 指令可以建立這種陣列。boolean 的陣列型別的訪問與修改共用 byte 型別陣列的 baload 和 bastore 指令。
三 引用型別與值 Java
虛擬機器中有三種引用型別:類型別(Class Types)、陣列型別(Array Types)和 介面型別(Interface Types)。這些引用型別的值分別由類例項、陣列例項和實現了某個介面 的類例項或陣列例項動態建立。
其中,陣列型別還包含一個單一維度(即長度不由其型別決定)的元件型別(Component Type),一個數組的元件型別也可以是陣列。但從任意一個數組開始,如果發現其元件型別也是數 組型別的話,繼續重複取這個陣列的元件型別,這樣操作不斷執行,最終一定可以遇到元件型別不 是陣列的情況,這時就把這種型別成為陣列型別的元素型別(Element Type)。陣列的元素型別 必須是原始型別、類型別或者介面型別之中的一種。
在引用型別的值中還有一個特殊的值:null,當一個引用不指向任何物件的時候,它的值就 用 null 來表示。一個為 null 的引用,在沒有上下文的情況下不具備任何實際的型別,但是有具 體上下文時它可轉型為任意的引用型別。引用型別的預設值就是 null。
Java 虛擬機器規範並沒有規定 null 在虛擬機器實現中應當怎樣編碼表示。
四 執行時資料區 Java
虛擬機器定義了若干種程式執行期間會使用到的執行時資料區,其中有一些會隨著虛擬機器 啟動而建立,隨著虛擬機器退出而銷燬。另外一些則是與執行緒一一對應的,這些與執行緒對應的資料區 域會隨著執行緒開始和結束而建立和銷燬。
4.1 PC 暫存器
Java 虛擬機器可以支援多條執行緒同時執行(可參考《Java 語言規範》第 17 章),每一條 Java 虛擬機器執行緒都有自己的 PC(Program Counter)暫存器。在任意時刻,一條 Java 虛擬機器執行緒 只會執行一個方法的程式碼,這個正在被執行緒執行的方法稱為該執行緒的當前方法(Current Method)。如果這個方法不是 native 的,那 PC 暫存器就儲存 Java 虛擬機器正在執行的 位元組碼指令的地址,如果該方法是 native 的,那 PC 暫存器的值是 undefined。PC 暫存器的容 量至少應當能儲存一個 returnAddress 型別的資料或者一個與平臺相關的本地指標的值。
4.2 Java 虛擬機器棧
每一條 Java 虛擬機器執行緒都有自己私有的 Java 虛擬機器棧(Java Virtual Machine Stack),這個棧與執行緒同時建立,用於儲存棧幀(Frames)。Java 虛擬機器棧的作用與傳統語 言(例如 C 語言)中的棧非常類似,就是用於儲存區域性變數與一些過程結果的地方。另外,它在 方法呼叫和返回中也扮演了很重要的角色。因為除了棧幀的出棧和入棧之外,Java 虛擬機器棧不會 再受其他因素的影響,所以棧幀可以在堆中分配,Java 虛擬機器棧所使用的記憶體不需要保證是連 續的。
Java 虛擬機器規範允許 Java 虛擬機器棧被實現成固定大小的或者是根據計算動態擴充套件和收縮的。如果採用固定大小的 Java 虛擬機器棧設計,那每一條執行緒的 Java 虛擬機器棧容量應當在執行緒創 建的時候獨立地選定。Java 虛擬機器實現應當提供給程式設計師或者終端使用者調節虛擬機器棧初始容量的 手段,對於可以動態擴充套件和收縮 Java 虛擬機器棧來說,則應當提供調節其最大、最小容量的手段。 Java 虛擬機器棧可能發生如下異常情況:
-
如果執行緒請求分配的棧容量超過 Java 虛擬機器棧允許的最大容量時,Java 虛擬機器將會丟擲一 個 StackOverflowError 異常。
-
如果 Java 虛擬機器棧可以動態擴充套件,並且擴充套件的動作已經嘗試過,但是目前無法申請到足夠的記憶體去完成擴充套件,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧,那 Java 虛 擬機將會丟擲一個 OutOfMemoryError 異常。
4.3 Java 堆
在 Java 虛擬機器中,堆(Heap)是可供各條執行緒共享的執行時記憶體區域,也是供所有類例項和陣列物件分配記憶體的區域。
Java 堆在虛擬機器啟動的時候就被建立,它儲存了被自動記憶體管理系統(Automatic Storage Management System,也即是常說的“Garbage Collector(垃圾收集器)”)所管理的各種 物件,這些受管理的物件無需,也無法顯式地被銷燬。本規範中所描述的 Java 虛擬機器並未假設採用什麼具體的技術去實現自動記憶體管理系統。虛擬機器實現者可以根據系統的實際需要來選擇自動記憶體管理技術。Java 堆的容量可以是固定大小的,也可以隨著程式執行的需求動態擴充套件,並在不需 要過多空間時自動收縮。Java 堆所使用的記憶體不需要保證是連續的。
Java 虛擬機器實現應當提供給程式設計師或者終端使用者調節 Java 堆初始容量的手段,對於可以動態擴充套件和收縮 Java 堆來說,則應當提供調節其最大、最小容量的手段。
Java 堆可能發生如下異常情況:
- 如果實際所需的堆超過了自動記憶體管理系統能提供的最大容量,那 Java 虛擬機器將會丟擲一個 OutOfMemoryError 異常。
4.4 方法區
在 Java 虛擬機器中,方法區(Method Area)是可供各條執行緒共享的執行時記憶體區域。方法區與傳統語言中的編譯程式碼儲存區(Storage Area Of Compiled Code)或者作業系統程序 的正文段(Text Segment)的作用非常類似,它儲存了每一個類的結構資訊,例如執行時常量 池(Runtime Constant Pool)、欄位和方法資料、建構函式和普通方法的位元組碼內容、還包 括一些在類、例項、介面初始化時用到的特殊方法。
方法區在虛擬機器啟動的時候被建立,雖然方法區是堆的邏輯組成部分,但是簡單的虛擬機器實現可以選擇在這個區域不實現垃圾收集。這個版本的 Java 虛擬機器規範也不限定實現方法區的記憶體位置和編譯程式碼的管理策略。方法區的容量可以是固定大小的,也可以隨著程式執行的需求動態擴充套件, 並在不需要過多空間時自動收縮。方法區在實際記憶體空間中可以是不連續的。 Java 虛擬機器實現應當提供給程式設計師或者終端使用者調節方法區初始容量的手段,對於可以動態 擴充套件和收縮方法區來說,則應當提供調節其最大、最小容量的手段。 方法區可能發生如下異常情況:
- 如果方法區的記憶體空間不能滿足記憶體分配請求,那 Java 虛擬機器將丟擲一個 OutOfMemoryError 異常。
4.5 執行時常量池
執行時常量池(Runtime Constant Pool)是每一個類或介面的常量池(Constant_Pool)的執行時表示形式,它包括了若干種不同的常量:從編譯期可知的數值字面量到必須執行 期解析後才能獲得的方法或欄位引用。執行時常量池扮演了類似傳統語言中符號表(Symbol Table)的角色,不過它儲存資料範圍比通常意義上的符號表要更為廣泛。
每一個執行時常量池都分配在 Java 虛擬機器的方法區之中,在類和介面被載入到虛擬機器後,對應的執行時常量池就被創建出來。
在建立類和介面的執行時常量池時,可能會發生如下異常情況:
- 當建立類或介面的時候,如果構造執行時常量池所需要的記憶體空間超過了方法區所能提供的最 大值,那 Java 虛擬機器將會丟擲一個 OutOfMemoryError 異常。
4.6 本地方法棧
Java 虛擬機器實現可能會使用到傳統的棧(通常稱之為“C Stacks”)來支援 native 方法 (指使用 Java 以外的其他語言編寫的方法)的執行,這個棧就是本地方法棧(Native Method Stack)。當 Java 虛擬機器使用其他語言(例如 C 語言)來實現指令集直譯器時,也會使用到本地 方法棧。如果 Java 虛擬機器不支援 natvie 方法,並且自己也不依賴傳統棧的話,可以無需支援本 地方法棧,如果支援本地方法棧,那這個棧一般會線上程建立的時候按執行緒分配。
Java 虛擬機器規範允許本地方法棧被實現成固定大小的或者是根據計算動態擴充套件和收縮的。如 果採用固定大小的本地方法棧,那每一條執行緒的本地方法棧容量應當在棧建立的時候獨立地選定。 一般情況下,Java 虛擬機器實現應當提供給程式設計師或者終端使用者調節虛擬機器棧初始容量的手段,對 於長度可動態變化的本地方法棧來說,則應當提供調節其最大、最小容量的手段。 本地方法棧可能發生如下異常情況:
-
如果執行緒請求分配的棧容量超過本地方法棧允許的最大容量時,Java 虛擬機器將會丟擲一個 StackOverflowError 異常。
-
如果本地方法棧可以動態擴充套件,並且擴充套件的動作已經嘗試過,但是目前無法申請到足夠的記憶體 去完成擴充套件,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的本地方法棧,那 Java 虛擬 機將會丟擲一個 OutOfMemoryError 異常。
五 棧幀
棧幀(Frame)是用來儲存資料和部分過程結果的資料結構,同時也被用來處理動態連結 (Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。
棧幀隨著方法呼叫而建立,隨著方法結束而銷燬——無論方法是正常完成還是異常完成(丟擲 了在方法內未被捕獲的異常)都算作方法結束。棧幀的儲存空間分配在 Java 虛擬機器棧之中,每一個棧幀都有自己的區域性變量表(Local Variables,§2.6.1)、運算元棧(Operand Stack)和指向當前方法所屬的類的執行時常量池的引用。
區域性變量表和運算元棧的容量是在編譯期確定,並通過方法的 Code 屬性儲存及 提供給棧幀使用。因此,棧幀容量的大小僅僅取決於 Java 虛擬機器的實現和方法呼叫時可被分配的 記憶體。
在一條執行緒之中,只有目前正在執行的那個方法的棧幀是活動的。這個棧幀就被稱為是當前棧 幀(Current Frame),這個棧幀對應的方法就被稱為是當前方法(Current Method),定義 這個方法的類就稱作當前類(Current Class)。對區域性變量表和運算元棧的各種操作,通常都 指的是對當前棧幀的對區域性變量表和運算元棧進行的操作。
如果當前方法呼叫了其他方法,或者當前方法執行結束,那這個方法的棧幀就不再是當前棧幀 了。當一個新的方法被呼叫,一個新的棧幀也會隨之而建立,並且隨著程式控制權移交到新的方法 而成為新的當前棧幀。當方法返回的之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,在方法返回之後,當前棧幀就隨之被丟棄,前一個棧幀就重新成為當前棧幀了。
請讀者特別注意,棧幀是執行緒本地私有的資料,不可能在一個棧幀之中引用另外一條執行緒的棧幀。
5.1 區域性變量表
每個棧幀內部都包含一組稱為區域性變量表(Local Variables)的變數列表。棧幀中區域性變量表的長度由編譯期決定,並且儲存於類和介面的二進位制表示之中,既通過方法的 Code 屬性儲存及提供給棧幀使用。 一個區域性變數可以儲存一個型別為 boolean、byte、char、short、float、reference 和 returnAddress 的資料,兩個區域性變數可以儲存一個型別為 long 和 double 的資料。 區域性變數使用索引來進行定位訪問,第一個區域性變數的索引值為零,區域性變數的索引值是從零 至小於區域性變量表最大容量的所有整數。 long 和 double 型別的資料佔用兩個連續的區域性變數,這兩種型別的資料值採用兩個區域性變 量之中較小的索引值來定位。例如我們講一個 double 型別的值儲存在索引值為 n 的區域性變數中, 實際上的意思是索引值為 n 和 n+1 的兩個區域性變數都用來儲存這個值。索引值為 n+1 的區域性變數 是無法直接讀取的,但是可能會被寫入,不過如果進行了這種操作,就將會導致區域性變數 n 的內 容失效掉。
上文中提及的區域性變數 n 的 n 值並不要求一定是偶數,Java 虛擬機器也不要求 double 和 long 型別資料採用 64 位對其的方式存放在連續的區域性變數中。虛擬機器實現者可以自由地選擇適當的方 式,通過兩個區域性變數來儲存一個 double 或 long 型別的值。
Java 虛擬機器使用區域性變量表來完成方法呼叫時的引數傳遞,當一個方法被呼叫的時候,它的 引數將會傳遞至從 0 開始的連續的區域性變量表位置上。特別地,當一個例項方法被呼叫的時候, 第 0 個區域性變數一定是用來儲存被呼叫的例項方法所在的物件的引用(即 Java 語言中的“this” 關鍵字)。後續的其他引數將會傳遞至從 1 開始的連續的區域性變量表位置上。
5.2 運算元棧
每一個棧幀(§2.6)內部都包含一個稱為運算元棧(Operand Stack)的後進先出 (Last-In-First-Out,LIFO)棧。棧幀中運算元棧的長度由編譯期決定,並且儲存於類和接 口的二進位制表示之中,既通過方法的 Code 屬性(§4.7.3)儲存及提供給棧幀使用。 在上下文明確,不會產生誤解的前提下,我們經常把“當前棧幀的運算元棧”直接簡稱為“操 作數棧”。 運算元棧所屬的棧幀在剛剛被建立的時候,運算元棧是空的。Java 虛擬機器提供一些位元組碼指 令來從區域性變量表或者物件例項的欄位中複製常量或變數值到運算元棧中,也提供了一些指令用於 從運算元棧取走資料、操作資料和把操作結果重新入棧。在方法呼叫的時候,運算元棧也用來準備 呼叫方法的引數以及接收方法返回結果。
舉個例子,iadd 位元組碼指令的作用是將兩個 int 型別的數值相加,它要求在執行的之前操作 數棧的棧頂已經存在兩個由前面其他指令放入的 int 型數值。在 iadd 指令執行時,2 個 int 值 從操作棧中出棧,相加求和,然後將求和結果重新入棧。在運算元棧中,一項運算常由多個子運算 (Subcomputations)巢狀進行,一個子運算過程的結果可以被其他外圍運算所使用。
每一個運算元棧的成員(Entry)可以儲存一個 Java 虛擬機器中定義的任意資料型別的值,包 括 long 和 double 型別。
在運算元棧中的資料必須被正確地操作,這裡正確操作是指對運算元棧的操作必須與運算元棧 棧頂的資料型別相匹配,例如不可以入棧兩個 int 型別的資料,然後當作 long 型別去操作他們, 或者入棧兩個 float 型別的資料,然後使用 iadd 指令去對它們進行求和。有一小部分 Java 虛擬機器指令(例如 dup 和 swap 指令)可以不關注運算元的具體資料型別,把所有在執行時資料區 中的資料當作裸型別(Raw Type)資料來操作,這些指令不可以用來修改資料,也不可以拆散那 些原本不可拆分的資料,這些操作的正確性將會通過 Class 檔案的校驗過程來強制保 障。
在任意時刻,運算元棧都會有一個確定的棧深度,一個 long 或者 double 型別的資料會佔用 兩個單位的棧深度,其他資料型別則會佔用一個單位深度。
六 動態連結
每一個棧幀內部都包含一個指向執行時常量池的引用來支援當前方法的程式碼實現動態連結(Dynamic Linking)。在 Class 檔案裡面,描述一個方法呼叫了其他方法, 或者訪問其成員變數是通過符號引用(Symbolic Reference)來表示的,動態連結的作用就是將這些符號引用所表示的方法轉換為實際方法的直接引用。類載入的過程中將要解析掉尚未被解析的符號引用,並且將變數訪問轉化為訪問這些變數的儲存結構所在的執行時記憶體位置的正確偏移 量。
由於動態連結的存在,通過晚期繫結(Late Binding)使用的其他類的方法和變數在發生變化時,將不會對呼叫它們的方法構成影響。
七 初始化方法的特殊命名
在 Java 虛擬機器層面上,Java 語言中的建構函式在《Java 語言規範 (第三版)》(下文簡稱 JLS3)是以一個名為的特殊例項初始化方法的形式出現的,這個方法名 稱是由編譯器命名的,因為它並非一個合法的 Java 方法名字,不可能通過程式編碼的方式實現。 例項初始化方法只能在例項的初始化期間,通過 Java 虛擬機器的 invokespecial 指令來呼叫, 只有在例項正在構造的時候,例項初始化方法才可以被呼叫訪問(JLS3)。
一個類或者介面最多可以包含不超過一個類或介面的初始化方法,類或者介面就是通過這個方 法完成初始化的。這個方法是一個不包含引數的靜態方法,名為 <clinit> 。這個名字也是由編譯器命名的,因為它並非一個合法的 Java 方法名字,不可能通過程式編碼的方式實現。 類或介面的初始化方法由 Java 虛擬機器自身隱式呼叫,沒有任何虛擬機器位元組碼指令可以呼叫這個方 法,只有在類的初始化階段中會被虛擬機器自身呼叫。
八 位元組碼指令集簡介
Java 虛擬機器的指令由一個位元組長度的、代表著某種特定操作含義的操作碼(Opcode)以及 跟隨其後的零至多個代表此操作所需引數的運算元(Operands)所構成。虛擬機器中許多指令並不 包含運算元,只有一個操作碼。
如果忽略異常處理,那 Java 虛擬機器的直譯器使用下面這個虛擬碼的迴圈即可有效地工作:
do { 自動計算 PC 暫存器以及從 PC 暫存器的位置取出操作碼;
if (存在運算元) 取出運算元;
執行操作碼所定義的操作 } while (處理下一次迴圈);
運算元的數量以及長度取決於操作碼,如果一個運算元的長度超過了一個位元組,那它將會以 Big-Endian 順序儲存——即高位在前的位元組序。舉個例子,如果要將一個 16 位長度的無符號整 數使用兩個無符號位元組儲存起來(將它們命名為 byte1 和 byte2),那它們的值應該是這樣的:
(byte1 << 8) | byte2
位元組碼指令流應當都是單位元組對齊的,只有“tableswitch”和“lookupswitch”兩條指 令例外,由於它們的運算元比較特殊,都是以 4 位元組為界劃分開的,所以這兩條指令那個也需要 預留出相應的空位來實現對齊。
限制 Java 虛擬機器操作碼的長度為一個位元組,並且放棄了編譯後代碼的引數長度對齊,是為了
8.1 資料型別與Java 虛擬機器
在 Java 虛擬機器的指令集中,大多數的指令都包含了其操作所對應的資料型別資訊。舉個例子, iload 指令用於從區域性變量表中載入 int 型的資料到運算元棧中,而 fload 指令載入的則是 float 型別的資料。這兩條指令的操作可能會是由同一段程式碼來實現的,但它們必須擁有各自獨 立的操作符。 對於大部分為與資料型別相關的位元組碼指令,他們的操作碼助記符中都有特殊的字元來表明專 門為哪種資料型別服務:i 代表對 int 型別的資料操作,l 代表 long,s 代表 short,b 代表 byte, c 代表 char,f 代表 float,d 代表 double,a 代表 reference。也有一些指令的助記符中沒 有明確的指明操作型別的字母,例如 arraylength 指令,它沒有代表資料型別的特殊字元,但 運算元永遠只能是一個數組型別的物件。還有另外一些指令,例如無條件跳轉指令 goto 則是與數 據型別無關的。
由於 Java 虛擬機器的操作碼長度只有一個位元組,所以包含了資料型別的操作碼對指令集的設計 帶來了很大的壓力:如果每一種與資料型別相關的指令都支援 Java 虛擬機器所有執行時資料型別的 話,那恐怕就會超出一個位元組所能表示的數量範圍了。因此,Java 虛擬機器的指令集對於特定的操 作只提供了有限的型別相關指令去支援它,換句話說,指令集將會故意被設計成非完全獨立的(Not Orthogonal,即並非每種資料型別和每一種操作都有對應的指令)。有一些單獨的指令可以在必 要的時候用來將一些不支援的型別轉換為可被支援的型別。
大部分的指令都沒有支援整數型別 byte、char 和 short,甚至沒有任何指令支援 boolean 型別。編譯器會在編譯期或執行期會將 byte 和 short 型別的資料 帶符號擴充套件(Sign-Extend)為相應的 int 型別資料,將 boolean 和 char 型別資料零位擴充套件 (Zero-Extend)為相應的 int 型別資料。與之類似的,在處理 boolean、byte、short 和 char 型別的陣列時,也會轉換為使用對應的 int 型別的位元組碼指令來處理。因此,大多數對於 boolean、byte、short 和 char 型別資料的操作,實際上都是使用相應的對 int 型別作為運 算型別(Computational Type)。
在 Java 虛擬機器中,實際型別與運算型別之間的對映關係,如表 2.3 所示。
8.2 載入和儲存指令
載入和儲存指令用於將資料從棧幀(§2.6)的區域性變量表(§2.6.1)和運算元棧之間來回 傳輸(§2.6.2):
-
將一個區域性變數載入到操作棧的指令包括有:iload、iload_、lload、lload_、 fload、fload_、dload、dload_、aload、aload_
-
將一個數值從運算元棧儲存到區域性變量表的指令包括有:istore、istore_、 lstore、lstore_、fstore、fstore_、dstore、dstore_、astore、 astore_
-
將一個常量載入到運算元棧的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、 aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_
-
擴充區域性變量表的訪問索引的指令:wide
訪問物件的欄位或陣列元素的指令也同樣會與運算元棧傳輸資料。
上面所列舉的指令助記符中,有一部分是以尖括號結尾的(例如 iload_),這些指令助 記符實際上是代表了一組指令(例如 iload_,它代表了 iload_0、iload_1、iload_2 和 iload_3 這幾條指令)。這幾組指令都是某個帶有一個運算元的通用指令(例如 iload)的特殊 形式,對於這若干組特殊指令來說,它們表面上沒有運算元,不需要進行取運算元的動作,但操作 數都是在指令中隱含的。除此之外,他們的語義與原生的通用指令完全一致(例如 iload_0 的語 義與運算元為 0 時的 iload 指令語義完全一致)。在尖括號之間的字母制定了指令隱含運算元的 資料型別,代表是 int 形資料,代表 long 型,代表 float 型,代表 double 型。在操作 byte、char 和 short 型別資料時,也用 int 型別表示。
8.2 型別轉換指令
型別轉換指令可以將兩種 Java 虛擬機器數值型別進行相互轉換,這些轉換操作一般用於實現用 戶程式碼的顯式型別轉換操作,或者用來處理 Java 虛擬機器位元組碼指令集中指令非完全獨立獨立的問題。
Java 虛擬機器直接支援(譯者注:“直接支援”意味著轉換時無需顯式的轉換指令)以下數值 的寬化型別轉換(Widening Numeric Conversions,小範圍型別向大範圍型別的安全轉換):
-
int 型別到 long、float 或者 double 型別
-
long 型別到 float、double 型別
-
float 型別到 double 型別
窄化型別轉換(Narrowing Numeric Conversions)指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。窄化型別轉換可能會導致轉換結果產生不同的正負號、不同的數 量級,轉換過程很可能會導致數值丟失精度。
在將 int 或 long 型別窄化轉換為整數型別 T 的時候,轉換過程僅僅是簡單的丟棄除最低位 N 個位元組以外的內容,N 是型別 T 的資料型別長度,這將可能導致轉換結果與輸入值有不同的正負號(譯者注:在高位位元組符號位被丟棄了)。
在將一個浮點值轉窄化轉換為整數型別 T(T 限於 int 或 long 型別之一)的時候,將遵循以下轉換規則:
-
如果浮點值是 NaN,那轉換結果就是 int 或 long 型別的 0
-
否則,如果浮點值不是無窮大的話,浮點值使用 IEEE 754 的向零舍入模式取整,獲得整數值 v,這時候可能有兩種情況:
-
如果 T 是 long 型別,並且轉換結果在 long 型別的表示範圍之內,那就轉換為 long型別數值 v
-
如果 T 是 int 型別,並且轉換結果在 int 型別的表示範圍之內,那就轉換為 int 型別數值 v
-
- 否則:
- 如果轉換結果 v 的值太小(包括足夠小的負數以及負無窮大的情況),無法使用 T 類 型表示的話,那轉換結果取 int 或 long 型別所能表示的最小數字。
- 如果轉換結果 v 的值太大(包括足夠大的正數以及正無窮大的情況),無法使用 T 類 型表示的話,那轉換結果取 int 或 long 型別所能表示的最大數字。
從 double 型別到 float 型別做窄化轉換的過程與 IEEE 754 中定義的一致,通過 IEEE 754 向最接近數舍入模式舍入得到一個可以使用 float 型別表示的數字。如果轉換結果的絕對值太小無法使用 float 來表示的話,將返回 float 型別的正負零。如果轉換結果的絕對值太大無法使用 float 來表示的話,將返回 float 型別的正負無窮大,對於 double 型別的 NaN 值將就規定轉換為 float 型別的 NaN 值。
儘管可能發生上限溢位、下限溢位和精度丟失等情況,但是 Java 虛擬機器中數值型別的窄化轉換永遠不可能導致虛擬機器丟擲執行時異常(此處的異常是指《Java 虛擬機器規範》中定義的異常, 請讀者不要與IEEE 754中定義的浮點異常訊號產生混淆)。
8.3 同步
Java 虛擬機器可以支援方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支援的。 方法級的同步是隱式,即無需通過位元組碼指令來控制的,它實現在方法呼叫和返回操作之中。
虛擬機器可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先持有管程, 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放管程。在方法執行期 間,執行執行緒持有了管程,其他任何執行緒都無法再獲得同一個管程。如果一個同步方法執行期間拋 出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的管程將在異常拋到同步方法 之外時自動釋放。
同步一段指令集序列通常是由 Java 語言中的 synchronized 塊來表示的,Java 虛擬機器的 指令集中有 monitorenter 和 monitorexit 兩條指令來支援 synchronized 關鍵字的語義, 正確實現 synchronized 關鍵字需要編譯器與 Java 虛擬機器兩者協作支援。
結構化鎖定(Structured Locking)是指在方法呼叫期間每一個管程退出都與前面的管程 進入相匹配的情形。因為無法保證所有提交給 Java 虛擬機器執行的程式碼都滿足結構化鎖定,所以 Java 虛擬機器允許(但不強制要求)通過以下兩條規則來保證結構化鎖定成立。假設 T 代表一條線 程,M 代表一個管程的話:
-
T 在方法執行時持有管程 M 的次數必須與 T 在方法完成(包括正常和非正常完成)時釋 放管程 M 的次數相等。
-
找方法呼叫過程中,任何時刻都不會出現執行緒 T 釋放管程 M 的次數比 T 持有管程 M 次數 多的情況。
請注意,在同步方法呼叫時自動持有和釋放管程的過程也被認為是在方法呼叫期間發生。