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 位有符號二進制補碼整數,默認值爲零。ide

  • int 類型:值爲 32 位有符號二進制補碼整數,默認值爲零。函數

  • long 類型:值爲 64 位有符號二進制補碼整數,默認值爲零。this

  • char 類型:值爲使用 16 位無符號整數表示的、指向基本多文本平面(Basic Multilingual Plane,BMP)的 Unicode 值,以 UTF-16 編碼,默認值爲 Unicode 的 null 值('\u0000')。編碼

浮點類型包括:spa

  • 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_m一、iconst_、lconst_、fconst_、dconst_

  • 擴充局部變量表的訪問索引的指令:wide

訪問對象的字段或數組元素的指令也一樣會與操做數棧傳輸數據。

上面所列舉的指令助記符中,有一部分是以尖括號結尾的(例如 iload_),這些指令助 記符其實是表明了一組指令(例如 iload_,它表明了 iload_0、iload_一、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 表明一個管程的話:

  1. T 在方法執行時持有管程 M 的次數必須與 T 在方法完成(包括正常和非正常完成)時釋 放管程 M 的次數相等。

  2. 找方法調用過程當中,任什麼時候刻都不會出現線程 T 釋放管程 M 的次數比 T 持有管程 M 次數 多的狀況。

請注意,在同步方法調用時自動持有和釋放管程的過程也被認爲是在方法調用期間發生。

相關文章
相關標籤/搜索