字节码指令
虛擬機(jī)是一個(gè)相對(duì)于物理機(jī)的概念,這兩種機(jī)器都有代碼執(zhí)行能力,其區(qū)別在于物理機(jī)的執(zhí)行引擎是直接建立在 CPU 處理器、指令集、操作系統(tǒng)和硬件層面上的。
而虛擬機(jī)的執(zhí)行引擎則由自己實(shí)現(xiàn),因此可以制定自己的指令集和執(zhí)行引擎的結(jié)構(gòu)體系,而且還可以執(zhí)行一些不被硬件直接支持的指令集格式。這就是虛擬機(jī)相對(duì)于物理機(jī)的優(yōu)勢(shì)所在。
但是缺點(diǎn)也比較明顯,由于多了一層虛擬指令,執(zhí)行虛擬機(jī)指令后還要轉(zhuǎn)化為本地機(jī)器碼,所以在執(zhí)行效率上,虛擬機(jī)是不如物理機(jī)的。
Java 虛擬機(jī)的字節(jié)碼指令由一個(gè)字節(jié)長(zhǎng)度的操作碼(Opcode)以及緊隨其后的零至多個(gè)操作數(shù)(Operands)構(gòu)成。
如果忽略異常處理,那么 Java 虛擬機(jī)的解釋器通過下面這個(gè)偽代碼的循環(huán)即可有效工作:
do{自動(dòng)計(jì)算pc寄存器以及從pc寄存器的位置取出操作碼;if(存在操作數(shù)){取出操作數(shù);}執(zhí)行操作碼所定義的操作; } while(處理下一次循環(huán));由于字節(jié)碼指令集限制了其操作碼長(zhǎng)度為 1 個(gè)字節(jié)(0 ~ 255),即意味著整個(gè)指令集中包含的指令總數(shù)不超過 256 條。
在虛擬機(jī)處理超過 1 個(gè)字節(jié)的數(shù)據(jù)時(shí),會(huì)在運(yùn)行時(shí)重新構(gòu)建出具體的數(shù)據(jù)結(jié)構(gòu)。
例如:如果要將一個(gè) 16 位無符號(hào)的整數(shù)使用兩個(gè)無符號(hào)字節(jié)存儲(chǔ)起來(命名為 byte1 和 byte2),那么這個(gè) 16 位無符號(hào)數(shù)的值應(yīng)該這樣表示:
(byte1 << 8) | byte2這種操作在某種程度上會(huì)導(dǎo)致執(zhí)行字節(jié)碼時(shí)損失一些性能。但這樣做的優(yōu)勢(shì)也非常明顯,放棄了操作數(shù)長(zhǎng)度對(duì)齊,就意味著可以節(jié)省很多填充和間隔符號(hào);用一個(gè)字節(jié)來代表操作碼,也是為了盡可能獲得短小精干的編譯代碼。
這種追求盡可能小數(shù)據(jù)量、高傳輸效率 的設(shè)計(jì)是由 Java 語言設(shè)計(jì)之初面向網(wǎng)絡(luò)、智能家電的技術(shù)背景所決定的并沿用至今。
字節(jié)碼與數(shù)據(jù)類型
在講字節(jié)碼指令之前,我們需要了解下,字節(jié)碼指令操作的操作數(shù)是什么類型的,這些 Java 虛擬機(jī)中的數(shù)值類型又和 Java 編程語言中的 8 大基本數(shù)據(jù)類型如何對(duì)應(yīng)的?
Java 語言中的 8 大基本數(shù)據(jù)類型:
- 整型:byte、short、int、long
- 浮點(diǎn)型:double、float
- 字符型:char
- 布爾型:boolean
Java 程序語言中定義了 8 大基本數(shù)據(jù)類型,但是在 Java 虛擬機(jī)中只分為兩大類:
- 原始類型(primitive type)
- 引用類型(reference type)
原始類型對(duì)應(yīng)的數(shù)值稱為原始值、引用類型的數(shù)值稱為引用值。
原始類型
原始類型包括如下類型。
- 數(shù)值類型
數(shù)值類型包括:byte、short、int、long、char、float、double。
- boolean 類型
boolean 類型的值有兩種:true 和 false,默認(rèn)為 false,雖然在 Java 虛擬機(jī)中定義了 boolean 這種類型,但是卻沒有指令直接支持其操作。
所以,對(duì) boolean 類型都需要在編譯后用虛擬機(jī)中的 int 類型來表示 —— 1 表示 true、0 表示 false。
- returnAddress 類型
returnAddress 類型表示一個(gè)指向某個(gè)操作碼 opcode 的指針,此操作碼與虛擬機(jī)指令相對(duì)應(yīng)。
引用類型
引用類型包括如下類型。
- 類類型(class type)
- 數(shù)組類型(array type)
- 接口類型(interface type)
這三種引用類型的值分別指向動(dòng)態(tài)創(chuàng)建的類實(shí)例、數(shù)組實(shí)例和實(shí)現(xiàn)了某個(gè)接口的類/數(shù)組實(shí)例。
在引用類型中還有一個(gè)特殊的值 null,當(dāng)一個(gè)引用不指向任何對(duì)象時(shí),它就用 null 表示, null 作為引用類型的初始默認(rèn)值可以轉(zhuǎn)型成任意的引用類型。
加載與存儲(chǔ)指令
加載和存儲(chǔ)指令用于將數(shù)據(jù)在棧幀中的局部變量表和操作數(shù)棧之間來回傳輸,這類指令包括如下內(nèi)容。
- 將一個(gè)局部變量加載到操作數(shù)棧
iload、iload_<n>、lload,load<n>、fload,fload_<n>、dload,dload_<n>、aload,aload_<n>
- 將一個(gè)數(shù)值從操作數(shù)棧存儲(chǔ)到局部變量表
istore、istore_<n>、lstore、lstore_<n>、fstore,fstore_<n>、dstore、dstore_<n>、astore,astore_<n>
- 將一個(gè)常量加載到操作數(shù)棧
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
- 擴(kuò)充局部變量表的訪問索引:wide
上面所列舉的指令助記符中,有一部分是以 _<n> 尾的,這些指令助記符實(shí)際上是代表了一組指令。
如:iload_<n> 代表了 iload_0、iload_1、iload_2 和 iload_3這幾條指令,此時(shí)操作數(shù)隱藏于指令之中。
iload_0 表示從當(dāng)前棧幀局部變量表中 0 號(hào)位置取 int 類型的數(shù)值加載到操作數(shù)棧 iload_1 表示從當(dāng)前棧幀局部變量表中 1 號(hào)位置取 int 類型的數(shù)值加載到操作數(shù)棧 ...運(yùn)算指令
算術(shù)指令用于對(duì)兩個(gè)操作數(shù)棧上的值進(jìn)行某種特定運(yùn)算,并把結(jié)構(gòu)重新壓入操作數(shù)棧。
大體上算術(shù)指令可以分為兩種:對(duì)整型數(shù)據(jù)進(jìn)行運(yùn)算的指令和對(duì)浮點(diǎn)類型數(shù)據(jù)進(jìn)行運(yùn)算的指令。
在每一大類中,都有針對(duì) Java 虛擬機(jī)具體數(shù)據(jù)類型的專用算術(shù)指令。但沒有直接支持 byte、short、char 和 boolean 類型的算術(shù)指令,對(duì)于這些數(shù)據(jù)的運(yùn)算,都是用 int 類型指令來處理。
所有算術(shù)指令包括:
- 加法指令:iadd、ladd、fadd、dadd
- 減法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 求負(fù)值指令:ineg、lneg、fneg、dneg
- 移位指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令:ior、lor
- 按位與指令:iand、land
- 按位異或指令:ixor、lxor
- 局部變量自增指令:iinc
- 比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
類型轉(zhuǎn)換指令
類型轉(zhuǎn)換指令可以在兩種 Java 虛擬機(jī)數(shù)值類型之間相互轉(zhuǎn)換。這些轉(zhuǎn)換操作一般用于實(shí)現(xiàn)用戶代碼中的顯式類型轉(zhuǎn)換操作,或者用來解決 Java 虛擬機(jī)字節(jié)碼指令的不完備問題。
Java 虛擬機(jī)直接支持以下數(shù)值的寬化類型轉(zhuǎn)換(widening numeric conversion,小范圍類型向大范圍類型的安全轉(zhuǎn)換):
- 從 int 類型到 long、float 或者 double 類型
- 從 long 類型到 float、double 類型
- 從 float 類型到 double 類型
寬化類型轉(zhuǎn)換指令包括:i2l、i2f、i2d、l2f、l2d 和 f2d。
Java 虛擬機(jī)也支持以下窄化類型轉(zhuǎn)換:
- 從 int 類型到 byte、short 或者 char 類型
- 從 long 類型到 int 類型
- 從 float 類型到 int 或者 long 類型
- 從 double 類型到 int、long 或者 float 類型
窄化類型轉(zhuǎn)換(narrowing numeric conversion)指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。
對(duì)象創(chuàng)建與訪問指令
在 Java 中類實(shí)例和數(shù)組都是對(duì)象,但是 Java 虛擬機(jī)對(duì)類 class 對(duì)象和數(shù)組對(duì)象的創(chuàng)建使用了不同的字節(jié)碼指令。
- 創(chuàng)建類實(shí)例的指令:new
- 創(chuàng)建數(shù)組的指令:newarray、anewarray、multianewarray
- 訪問類變量(static 字段)的指令:getstatic、putstatic
- 訪問實(shí)例變量的指令:getfield、putfield
- 將一個(gè)數(shù)組元素加載到操作數(shù)棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 將一個(gè)操作數(shù)棧的值存到數(shù)組元素中的指令:bastore、castore、sastore、iastor、fastore、dastore、aastore
- 取數(shù)組長(zhǎng)度的指令:arraylength
- 檢查類實(shí)例類型的指令:instanceof、checkcast
操作數(shù)棧管理指令
Java 虛擬機(jī)提供了一些用于直接操控操作數(shù)棧的指令,包括:pop、pop2、dup、dup2、dup_x1、dup_x2、dup2_x1、dup_x2、dup2_x2 和 swap。
控制轉(zhuǎn)移指令
控制轉(zhuǎn)移指令可以讓 Java 虛擬機(jī)有條件或無條件地從指定指令而不是控制轉(zhuǎn)移指令的下一條指令繼續(xù)執(zhí)行程序。
控制指令包括:
- 條件分支
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
方法調(diào)用與返回指令
以下 5 條指令用于方法調(diào)用:
- invokevirtual
用于調(diào)用對(duì)象的實(shí)例方法,根據(jù)對(duì)象的實(shí)際類型進(jìn)行分派(虛方法分派),這也是 Java 語言中最常見的方法分派方式。
- invokeinterface
用于調(diào)用接口方法,它會(huì)在運(yùn)行時(shí)搜索一個(gè)實(shí)現(xiàn)了此接口的對(duì)象,找出合適的方法進(jìn)行調(diào)用。
- invokestatic
用于調(diào)用類方法(static 方法)。
- invokedynamic
指令用于在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法,并執(zhí)行該方法,前面的 4 條調(diào)用指令的分派邏輯都固話在 Java 虛擬機(jī)內(nèi)部,而 invokedynamic 指令的分派邏輯則是由用戶所設(shè)定的引導(dǎo)方法所決定的。
異常處理指令
在 Java 程序中顯示拋出異常的操作(throw 語句)都由 athrow 指令來實(shí)現(xiàn),除了用 throw 語句顯示拋出的異常以外,Java 虛擬機(jī)規(guī)范還規(guī)定了許多會(huì)在 Java 虛擬機(jī)檢查到異常狀況時(shí)自動(dòng)拋出的運(yùn)行時(shí)異常。
如:在整數(shù)運(yùn)算中,當(dāng)除數(shù)為 0 時(shí),虛擬機(jī)會(huì)在 idiv 或 ldiv 指令中拋出 ArithmeticException 異常。
此處需要注意的是,在 Java 虛擬機(jī)中處理異常(catch 語句)不是由字節(jié)碼指令實(shí)現(xiàn)的,而是采用異常處理器(異常表)來完成的。
同步指令
Java 虛擬機(jī)可以支持方法級(jí)的同步和方法內(nèi)部一段指令序列的同步,兩種同步都是使用管程(Monitor)來支持的。
- 方法級(jí)的同步
方法級(jí)的同步時(shí)隱式的,即無需通過字節(jié)碼指令控制,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。虛擬機(jī)可以從方法常量池的方法表中 ACC_SYNCHRONIZED 訪問標(biāo)志得知此方法是否聲明為同步方法。
當(dāng)方法調(diào)用時(shí),如果此方法為同步方法,則執(zhí)行線程就要去先成功持有管程,然后才能執(zhí)行方法,方法(無論是否正常完成)完成后釋放管程。
如果這個(gè)同步方法執(zhí)行期間拋出異常,并且方法內(nèi)部無法處理,那么此方法持有的管程將在異常拋出去后自動(dòng)釋放。
- 指令序列級(jí)的同步
同步一段指令序列通常是由 Java 中的 synchronized 語句塊來表示的,Java 虛擬機(jī)指令集中有 monitorenter 和 monitorexit 兩條指令來支持 synchronized 關(guān)鍵字。
附錄
- 虛擬機(jī)字節(jié)碼指令表
參考資料
- 《深入理解 Java 虛擬機(jī)》
- 《Java 虛擬機(jī)規(guī)范 SE 8 版》
我的 GitHub
github.com/jeanboydev
我的公眾號(hào)
歡迎關(guān)注我的公眾號(hào),分享各種技術(shù)干貨,各種學(xué)習(xí)資料,職業(yè)發(fā)展和行業(yè)動(dòng)態(tài)。
技術(shù)交流群
歡迎加入技術(shù)交流群,來一起交流學(xué)習(xí)。
總結(jié)
- 上一篇: 使用STM32或GD32解析xml格式数
- 下一篇: ETL工具Kettle使用教程