一 數據類型
與 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 次數 多的情況。
請注意,在同步方法調用時自動持有和釋放管程的過程也被認為是在方法調用期間發生。
