03.01 Java虛擬機—字節碼指令初探

前言:

Java虛擬機指令是JVM的核心,JVM通過這些指令來取操作數、取引用關係再返回計算結果,從而完成Java中程序的實際執行過程。本文主要內容摘自《深入理解Java虛擬機》第二版-周志明和《Java虛擬機規範》-Java SE 8版,介紹了Java虛擬機中的字節碼指令,主要包括:

  • 加載和存儲指令、
  • 運算指令、
  • 類型轉換指令、
  • 對象創建與訪問指令、
  • 操作數棧管理指令
  • 控制轉移指令、
  • 方法調用和返回指令、
  • 異常處理指令和同步指令。



“虛擬機”是一個相對於“物理機”的概念,這兩種機器都有代碼執行能力,其區別在於物理機的執行引擎是直接建立在CPU處理器、指令集、操作系統和硬件層面上的;而虛擬機的執行引擎則由自己實現,因此可以制定自己的指令集和執行引擎的結構體系,而且還可以執行一些不被硬件直接支持的指令集格式。這就是虛擬機相對於物理機的優勢所在。但是缺點也比較明顯,由於多了一層虛擬指令,執行虛擬機指令後還要轉化為本地機器碼,所以在執行效率上,虛擬機是不如物理機的。

Java虛擬機的字節碼指令由1個字節長度的操作碼(Opcode)以及緊隨其後的0~多個操作數(Operands)構成。如果忽略異常處理,那麼Java虛擬機的解釋器通過下面這個偽代碼的循環即可有效工作:

<code>do{
自動計算pc寄存器以及從pc寄存器的位置取出操作碼;
if(存在操作數){
取出操作數;
}
執行操作碼所定義的操作;
} while(處理下一次循環);/<code>

由於字節碼指令集限制了其操作碼長度為1個字節(0~255),即意味著整個指令集中包含的指令總數不超過256條。在虛擬機處理超過1個字節的數據時,會在運行時重新構建出具體的數據結構。譬如:如果要將一個16位無符號的整數使用兩個無符號字節存儲起來(命名為byte1和byte2)那麼這個16位無符號數的值應該這樣表示:(byte1 << 8) | byte2

這種操作在某種程度上會導致執行字節碼時損失一些性能。但這樣做的優勢也非常明顯,放棄了操作數長度對齊,就意味著可以節省很多填充和間隔符號;用一個字節來代表操作碼,也是為了儘可能獲得短小精幹的編譯代碼。這種追求儘可能小數據量、高傳輸效率 的設計是由Java語言設計之初面向網絡、智能家電的技術背景所決定的並沿用至今。



Java語言中的8大基本數據類型和Java虛擬機中的數據類型

在講字節碼指令之前,我們需要了解下,字節碼指令操作的操作數是什麼類型的,這些Java虛擬機中的數值類型又和Java編程語言中的8大基本數據類型如何對應的?

Java編程語言中的8大基本數據類型整型:byte、short、int、long

浮點型:double、float

字符型:char

布爾型:boolean

Java虛擬機中的數據類型

Java程序語言中定義了8大基本數據類型,但是在虛擬機中只分為兩大類:

  • 1.原始類型(primitive type)
  • 2.引用類型(reference type)

原始類型對應的數值稱為原始值、引用類型的數值稱為引用值。

1.原始類型包括:

  • 數值類型
  • boolean類型
  • returnAddress類型

數值類型包括:byte、short、int、long、char、float、double。

boolean類型的值有兩種:true和false,默認為false,雖然在Java虛擬機中定義了boolean這種類型,但是卻沒有指令直接支持其操作,所以,對boolean類型都需要在編譯後用虛擬機中的int類型來表示——1表示true、0表示false。

returnAddress類型表示一個指向某個操作碼opcode的指針,此操作碼與虛擬機指令相對應)

以上可以看出,在Java虛擬機的「原始類型」中,除了returnAddress,其餘都是和Java語言中的8大基本數據類型一一對應的。那麼「引用類型」呢?很明顯,「引用類型」和Java中對象的引用有關,在虛擬機中,對象表示為某個類的實例或者某個數組,指向此對象的引用就用引用類型(reference)來表示。關於reference類型的值,可以比作指向對象的指針,每個對象可能存在多個指向它的引用reference,對象的操作、傳遞和檢查都通過引用它的reference類型數據來進行。

2.引用類型包括

  • 類類型(class type)
  • 數組類型(array type)
  • 接口類型(interface type)

這三種引用類型的值分別指向動態創建的類實例、數組實例和實現了某個接口的類/數組實例,在引用類型中還有一個特殊的值null,當一個引用不指向任何對象時,它就用null表示,null作為引用類型的初始默認值可以轉型成任意的引用類型。



1.字節碼與數據類型

在java虛擬機的指令集中,大多數指令都包含了其操作數對應的數據類型信息。如:iload指令用於從局部變量表中加載int型數據到操作數棧,fload指令加載的是float類型的數據,而aload指令加載的是一個引用(reference)類型的數據。這些指令都是和數據類型相關的指令,即指令中直接包含了相應操作數的數據類型信息。類似的指令中,i代表int類型的數據類型、l代表long、s代表short、c代表char、f代表float、d代表double、a代表reference。

還有一些沒有明確指示操作類型的指令如arraylength指令,其操作數只能是一個數組類型的對象、goto指令表示無條件跳轉也和數據類型無關。

在Java虛擬機指令集設計的過程中,由於操作碼長度為1字節,導致所有指令的總數必須控制在256個以內,而Java有8大基本數據類型(byte,short,int,long,float,double,char,boolean)

如果給每種類型都設計一套指令,那麼就會產生8套重複冗餘的指令,指令總數肯定是超過256個的,所以為了避免這個問題,Java虛擬機指令集在設計時刻意避開了一些數據類型:

大部分的指令都沒有直接支持byte、char、short、boolean類型數據。編譯器會在編譯器或運行期將byte和short類型的數據(採用了類型轉換指令)帶符號拓展為相應的int類型數據,將boolean和char類型數據零位拓展為相應int類型數據。因此,大多數對java中boolean、byte、short和char類型數據的操作,實際上都是使用JVM中int類型作為運算類型來操作的。



1.加載和存儲指令

加載和存儲指令用於將數據在棧幀中的局部變量表和操作數棧之間來回傳輸,這類指令包括如下內容。

將一個局部變量加載到操作數棧:

iload,iload_,lload,load,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_0、iload_1、iload_2和iload_3這幾條指令,此時操作數隱藏於指令之中。

<code>iload_0表示從當前棧幀局部變量表中0號位置取int類型的數值加載到操作數棧
iload_1表示從當前棧幀局部變量表中1號位置取int類型的數值加載到操作數棧
.../<code>
Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

2.運算指令

運算OR算術指令用於對兩個操作數棧上的值進行某種運算,並把結果重新存儲到操作數棧頂。運算指令大體上可分為2種:整型數據運算、浮點型數據運算。

無論是整型還是浮點型、一律採用Java虛擬機中的數據類型,而不是Java語法中的八大基本數據類型。由於沒有對byte、short、char和boolean類型直接支持的算術指令,故這些類型,會使用int型的指令代替。

加法指令:xadd

減法指令:xsub

乘法指令: xmul

除法指令:xdiv

求餘指令:xrem

取反指令:xneg

以上x=i,l,f,d分別表示int型、long型、float型、double型

位移指令:ishl,ishr,iushr, lshl,lshr,lushr

按位或指令:ior , lor

按位與指令:iand , land

按位異或指令:ixor , lxor

局部變量自增指令:iinc

比較指令:dcmpg dcmpl, fcmpg ,fcmpl, lcmp

Java虛擬機—字節碼指令初探

3.類型轉換指令

類型轉換指令可以將兩種不同的數值類型進行相互轉換。Java虛擬機直接支持(無需轉換指令)以下數值類型的寬化類型轉換(即小範圍類型向大範圍類型的安全轉換):

int 類型到 long,float,或者double

long類型到float,double

float類型到double

相對的,處理窄化類型轉換時,需要通過相應的類型轉換指令來完成,這些指令包括:

i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f

4.對象創建和訪問指令

在Java中類實例和數組都是對象,但是JVM對類Class對象和數組對象的創建使用了不同的字節碼指令。

創建類實例的指令:new

創建數組的指令:newarray,anewarray,multianewarray

訪問類變量(static字段)的指令:getstatic,putstatic

訪問實例變量的指令:getfield,putfield

將一個數組元素加載到操作數棧的指令:baload,caload,saload,iaload,laload,faload,daload,aaload

將一個操作數棧的值存到數組元素中的指令:

bastore,castore,sastore,iastore,fastore,dastore,aastore.

取數組長度的指令:arraylength

檢查類實例類型的指令:instanceof,checkcast

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

5.操作數棧管理指令

將操作數棧棧頂元素出棧:pop

將操作數棧棧頂2個元素出棧:pop2

複製棧頂1個或2個數值,並將複製的值重新壓入棧頂:

dup,dup2,dupx1,dup2_x1,dup_x2,dup2_x2

將棧頂兩個數值互換:swap

Java虛擬機—字節碼指令初探

Java虛擬機—字節碼指令初探

6.控制轉移指令

控制轉移指令可以讓Java虛擬機從指定位置的指令繼續執行(而不是當前指令的下一條指令),所以從概念模型上理解,可以認為控制轉移指令就是在有條件或無條件地修改PC寄存器的值。控制轉移指令如下:

條件分支:

ifeq,iflt,ifle,ifne,ifgt,ifge,jfnull,ifnonnull,ificmpeq,ificmpne,ificmplt,ificmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne

複合條件分支:tableswitch,lookupswitch

無條件分支:goto,goto_w,jsr,jsr_w,ret

7.方法調用和返回指令

方法調用指令

方法調用(分派、執行過程)指令非常重要,這裡僅列舉5條常用的方法調用指令:

invokevirtual:用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。

invokeinterface:用於調用接口方法,它會在運行時搜索一個實現了此接口的對象,找出合適的方法進行調用。

invokespecial:用於調用一些需要特殊處理的實例方法、包括實例初始化方法、私有方法和父類方法。

invokestatic:用於調用類方法(static方法)

invokedynamic:指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法,前面的4條調用指令的分派邏輯都固話在Java虛擬機內部,而invokedynamic指令的分派邏輯則是由用戶所設定的引導方法所決定的。

Java虛擬機—字節碼指令初探

方法返回指令

方法調用指令與數據類型無關, 方法返回指令是根據返回值的類型區分的,包括:

return:提供聲明為void的方法、實例初始化方法、類和接口的類初始化方法使用

ireturn:返回int類型的數據,當返回值是boolean、byte、char、short和int時使用

其他類型的返回指令:lreturn、freturn、dreturn、areturn

Java虛擬機—字節碼指令初探

8.異常處理指令

在Java程序中顯示拋出異常的操作(throw語句)都由athrow指令來實現,除了用throw語句顯示拋出的異常以外,Java虛擬機規範還規定了許多會在JVM檢查到異常狀況時自動拋出的運行時異常。如在整數運算中,當除數為0時,虛擬機會在idiv或ldiv指令中拋出ArithmeticException異常。

此處需要注意的是,在Java虛擬機中處理異常(catch語句)不是由字節碼指令實現的,而是採用異常處理器(異常表)來完成的。

Java虛擬機—字節碼指令初探

由Java虛擬機執行的每個方法都會配有零至多個異常處理器(Exception handler),異常處理器描述了其在方法代碼中的有效作用範圍、能處理的異常類型以及處理異常代碼所在的位置。要判斷某個異常處理器是否可以處理某個具體的異常,需要同時檢查異常出現的位置是否在異常處理器的有效作用範圍內,以及異常是否為其聲明的可以處理的異常類型。

當異常拋出時,Java虛擬機搜索當前方法中的所有異常處理器,如果能匹配,則將代碼控制權轉向異常處理器的分支之中,否則丟棄當前方法操作數棧和局部變量表,並將當前方法的棧幀出虛擬機棧,程序恢復到當前方法的調用者的棧幀中。

示例如下,是一個方法test2()的字節碼指令,其中Code部分是字節碼指令(行號0~29),Exception table中為異常表,定義了一個異常處理器:

<code>public static int test2(int);
descriptor: (I)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iinc 0, 20
3: iload_0
4: istore_1
5: iinc 0, 30
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_0
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: iload_1
16: ireturn
17: astore_2
18: iinc 0, 30
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_0
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: aload_2
29: athrow
Exception table:
from to target type
0 5 17 any
LineNumberTable:
line 17: 0
line 18: 3
line 20: 5

line 21: 8
line 18: 15
line 20: 17
line 21: 21
line 22: 28
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 b I
StackMapTable: number_of_entries = 1
frame_type = 81 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]/<code>

重點看一下這個test2()方法的異常表:

<code>Exception table:
from to target type
0 5 17 any/<code>

異常表中有一項內容,即一個異常處理器。from0,to5,target17,any表示了在第0~5範圍內的字節碼指令內若發生異常,且異常的類型type為任意類型時,程序跳轉到第17行繼續執行。如果異常發生在5以後,則不能被此異常處理器捕獲,此時test2()方法的局部變量表已經操作數棧將被清空,test2()方法棧幀將被銷燬,程序返回到test2()方法的調用者處繼續執行。

9.同步指令

Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,兩種同步都是使用管程(Monitor)來支持的。

方法級的同步

方法級的同步時隱式的,即無需通過字節碼指令控制,它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表中ACC_SYNCHRONIZED訪問標誌得知此方法是否聲明為同步方法。當方法調用時,如果此方法為同步方法,則執行線程就要去先成功持有管程,然後才能執行方法,方法(無論是否正常完成)完成後釋放管程。如果這個同步方法執行期間拋出異常,並且方法內部無法處理,那麼此方法持有的管程將在異常拋出去後自動釋放。

指令序列級的同步

同步一段指令序列通常是由Java中的synchronized語句塊來表示的,Java虛擬機指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字。



以上就是JVM指令系統的簡單介紹,在以後的文章中,我們會經常用到javap -v xxx.class反編譯一個類的字節碼,就會經常見到這些指令,見的多了自然就熟悉了!


分享到:


相關文章: