歡迎來(lái)到“Under The Hood“第三期。前兩期我們分別介紹了JVM的基本結(jié)構(gòu)和功能和Java類文件的基本結(jié)構(gòu),本期的主要內(nèi)容有:字節(jié)碼所操作的原始類型、類型轉(zhuǎn)換的字節(jié)碼,以及操作JVM棧的字節(jié)碼。
字節(jié)碼格式
字節(jié)碼是JVM的機(jī)器語(yǔ)言。JVM加載類文件時(shí),對(duì)類中的每個(gè)方法,它都會(huì)得到一個(gè)字節(jié)碼流。這些字節(jié)碼流保存在JVM的方法區(qū)中。在程序運(yùn)行過(guò)程中,當(dāng)一個(gè)方法被調(diào)用時(shí),它的字節(jié)碼流就會(huì)被執(zhí)行。根據(jù)特定JVM設(shè)計(jì)者的選擇,它們可以通過(guò)解釋的方式,即時(shí)編譯(Just-in-time compilation)的方式或其他技術(shù)的方式被執(zhí)行。
方法的字節(jié)碼流就是JVM的指令(instruction)序列。每條指令包含一個(gè)單字節(jié)的操作碼(opcode)和0個(gè)或多個(gè)操作數(shù)(operand)。操作碼指明要執(zhí)行的操作。如果JVM在執(zhí)行操作前,需要更多的信息,這些信息會(huì)以0個(gè)或多個(gè)操作數(shù)的方式,緊跟在操作碼的后面。
每種類型的操作碼都有一個(gè)助記符(mnemonic)。類似典型的匯編語(yǔ)言風(fēng)格,Java字節(jié)碼流可以用它們的助記符和緊跟在后面的操作數(shù)來(lái)表示。例如,下面的字節(jié)碼流可以分解成多個(gè)助記符的形式。
-
// 字節(jié)碼流: 03 3b 84 00 01 1a 05 68 3b a7 ff f9
-
// 分解后:
-
iconst_0 // 03
-
istore_0 // 3b
-
iinc 0, 1 // 84 00 01
-
iload_0 // 1a
-
iconst_2 // 05
-
imul // 68
-
istore_0 // 3b
-
goto -7 // a7 ff f9
字節(jié)碼指令集被設(shè)計(jì)的很緊湊。除了處理跳表的2條指令以外,所有的指令都以字節(jié)邊界對(duì)齊。操作碼的總數(shù)很少,一個(gè)字節(jié)就能搞定。這最小化了JVM加載前,通過(guò)網(wǎng)絡(luò)傳輸?shù)念愇募拇笮。灰彩沟肑VM可以維持很小的實(shí)現(xiàn)。
JVM中,所有的計(jì)算都是圍繞棧(stack)而展開(kāi)的。因?yàn)镴VM沒(méi)有存儲(chǔ)任意數(shù)值的寄存器(register),所有的操作數(shù)在計(jì)算開(kāi)始之前,都必須先壓入棧中。因此,字節(jié)碼指令主要是用來(lái)操作棧的。例如,在上面的字節(jié)碼序列中,通過(guò)iload_0先把本地變量(local variable)入棧,然后用iconst_2把數(shù)字2入棧的方式,來(lái)計(jì)算本地變量乘以2。兩個(gè)整數(shù)都入棧之后,imul指令有效的從棧中彈出它們,然后做乘法,最后把運(yùn)算結(jié)果壓入棧中。istore_0指令把結(jié)果從棧頂彈出,保存回本地變量。JVM被設(shè)計(jì)成基于棧,而不是寄存器的機(jī)器,這使得它在如80486寄存器架構(gòu)不佳的處理器上,也能被高效的實(shí)現(xiàn)。
原始類型(primitive types)
JVM支持7種原始數(shù)據(jù)類型。Java程序員可以聲明和使用這些數(shù)據(jù)類型的變量,而Java字節(jié)碼,處理這些數(shù)據(jù)類型。下表列出了這7種原始數(shù)據(jù)類型:
類型
|
定義
|
byte |
單字節(jié)有符號(hào)二進(jìn)制補(bǔ)碼整數(shù) |
short |
2字節(jié)有符號(hào)二進(jìn)制補(bǔ)碼整數(shù) |
int |
4字節(jié)有符號(hào)二進(jìn)制補(bǔ)碼整數(shù) |
long |
8字節(jié)有符號(hào)二進(jìn)制補(bǔ)碼整數(shù) |
float |
4字節(jié)IEEE 754單精度浮點(diǎn)數(shù) |
double |
8字節(jié)IEEE 754雙精度浮點(diǎn)數(shù) |
char |
2字節(jié)無(wú)符號(hào)Unicode字符 |
原始數(shù)據(jù)類型以操作數(shù)的方式出現(xiàn)在字節(jié)碼流中。所有長(zhǎng)度超過(guò)1字節(jié)的原始類型,都以大端(big-endian)的方式保存在字節(jié)碼流中,這意味著高位字節(jié)出現(xiàn)在低位字節(jié)之前。例如,為了把常量值256(0×0100)壓入棧中,你可以用sipush操作碼,后跟一個(gè)短操作數(shù)。短操作數(shù)會(huì)以“01 00”的方式出現(xiàn)在字節(jié)碼流中,因?yàn)镴VM是大端的。如果JVM是小端(little-endian)的,短操作數(shù)將會(huì)是“00 01”。
-
// Bytecode stream: 17 01 00
-
// Dissassembly:
-
sipush 256; // 17 01 00
把常量(constants)壓入棧中
很多操作碼都可以把常量壓入棧中。操作碼以3中不同的方式指定入棧的常量值:由操作碼隱式指明,作為操作數(shù)跟在操作碼之后,或者從常量池(constant pool)中獲取。
有些操作碼本身就指明了要入棧的數(shù)據(jù)類型和常量數(shù)值。例如,iconst_1告訴JVM把整數(shù)1壓入棧中。這種操作碼,是為不同類型而經(jīng)常入棧的數(shù)值而定義的。它們?cè)谧止?jié)碼流中只占用1個(gè)字節(jié),增進(jìn)了字節(jié)碼的執(zhí)行效率,并減小了字節(jié)碼流的大小。下表列出了int型和float型的操作碼:
操作碼
|
操作數(shù)
|
描述
|
iconst_m1 |
(none) |
pushes int -1 onto the stack |
iconst_0 |
(none) |
pushes int 0 onto the stack |
iconst_1 |
(none) |
pushes int 1 onto the stack |
iconst_2 |
(none) |
pushes int 2 onto the stack |
iconst_3 |
(none) |
pushes int 3 onto the stack |
iconst_4 |
(none) |
pushes int 4 onto the stack |
iconst_5 |
(none) |
pushes int 5 onto the stack |
fconst_0 |
(none) |
pushes float 0 onto the stack |
fconst_1 |
(none) |
pushes float 1 onto the stack |
fconst_2 |
(none) |
pushes float 2 onto the stack |
下面列出的操作碼處理的int型和float型都是32位的值。Java棧單元(slot)是32位寬的,因此,每次一個(gè)int數(shù)和float數(shù)入棧,它都占用一個(gè)單元。下表列出的操作碼處理long型和double型。long型和double型的數(shù)值占用64位。每次一個(gè)long數(shù)或double數(shù)被壓入棧中,它都占用2個(gè)棧單元。下面的表格,列出了隱含處理long型和double型的操作碼
操作碼
|
操作數(shù)
|
描述
|
lconst_0 |
(none) |
pushes long 0 onto the stack |
lconst_1 |
(none) |
pushes long 1 onto the stack |
dconst_0 |
(none) |
pushes double 0 onto the stack |
dconst_1 |
(none) |
pushes double 1 onto the stack |
另外還有一個(gè)隱含入棧常量值的操作碼,aconst_null,它把空對(duì)象(null object)的引用(reference)壓入棧中。對(duì)象引用的格式取決于JVM實(shí)現(xiàn)。對(duì)象引用指向垃圾收集堆(garbage-collected heap)中的對(duì)象。空對(duì)象引用,意味著一個(gè)變量當(dāng)前沒(méi)有指向任何合法對(duì)象。aconst_null操作碼用在給引用變量賦null值的時(shí)候。
操作碼
|
操作數(shù)
|
描述
|
aconst_null |
(none) |
pushes a null object reference onto the stack |
有2個(gè)操作碼需要緊跟一個(gè)操作數(shù)來(lái)指明入棧的常量值。下表列出的操作碼,用來(lái)把合法的byte型和short型的常量值壓入棧中。byte型或short型的值在入棧之前,先被擴(kuò)展成int型的值,因?yàn)闂卧?2位寬的。對(duì)byte型和short型的操作,實(shí)際上是基于它們擴(kuò)展后的int型值的。
操作碼
|
操作數(shù)
|
描述
|
bipush |
byte1 |
expands byte1 (a byte type) to an int and pushes it onto the stack |
sipush |
byte1, byte2 |
expands byte1, byte2 (a short type) to an int and pushes it onto the stack |
有3個(gè)操作碼把常量池中的常量值壓入棧中。所有和類關(guān)聯(lián)的常量,如final變量,都被保存在類的常量池中。把常量池中的常量壓入棧中的操作碼,都有一個(gè)操作數(shù),它表示需要入棧的常量在常量池中的索引。JVM會(huì)根據(jù)索引查找常量,確定它的類型,并把它壓入棧中。
在字節(jié)碼流中,常量池索引(constant pool index)是一個(gè)緊跟在操作碼后的無(wú)符號(hào)值。操作碼lcd1和lcd2把32位的項(xiàng)壓入棧中,如int或float。兩者的區(qū)別在于lcd1只適用于1-255的常量池索引位,因?yàn)樗乃饕挥?個(gè)字節(jié)。(常量池0號(hào)位未被使用。)lcd2的索引有2個(gè)字節(jié),所以它可以適用于常量池的任意位置。lcd2w也有一個(gè)2字節(jié)的索引,它被用來(lái)指示任意含有64位的long或double型數(shù)據(jù)的常量池位置。下表列出了把常量池中的常量壓入棧中的操作碼:
操作碼
|
操作數(shù)
|
描述
|
ldc1 |
indexbyte1 |
pushes 32-bit constant_pool entry specified by indexbyte1 onto the stack |
ldc2 |
indexbyte1, indexbyte2 |
pushes 32-bit constant_pool entry specified by indexbyte1, indexbyte2 onto the stack |
ldc2w |
indexbyte1, indexbyte2 |
pushes 64-bit constant_pool entry specified by indexbyte1,indexbyte2 onto the stack |
把局部變量(local variables)壓入棧中
局部變量保存在棧幀的一個(gè)特殊區(qū)域中。棧幀是當(dāng)前執(zhí)行方法正在使用的棧區(qū)。每個(gè)棧幀包含3個(gè)部分:本地變量區(qū),執(zhí)行環(huán)境和操作數(shù)棧區(qū)。把本地變量入棧實(shí)際上包含了把數(shù)值從棧幀的本地變量區(qū)移動(dòng)到操作數(shù)棧區(qū)。操作數(shù)棧區(qū)總是在棧的頂部,所以,把一個(gè)值壓到當(dāng)前棧幀的操作數(shù)棧區(qū)頂部,跟壓到整個(gè)JVM棧的頂部是一個(gè)意思。
Java棧是一個(gè)先進(jìn)后出(LIFO)的32位寬的棧。所有的本地變量至少占用32位,因?yàn)闂V械拿總€(gè)單元都是32位寬的。像long和double類型的64位的本地變量會(huì)占用2個(gè)棧單元。byte和short型的本地變量會(huì)當(dāng)做int型來(lái)存儲(chǔ),但只擁有較小類型的合法值。例如,表示byte型的int型本地變量取值范圍總是-128到127。
每個(gè)本地變量都有一個(gè)唯一索引。方法棧幀的本地變量區(qū),可以當(dāng)成是一個(gè)擁有32位寬的元素的數(shù)組,每個(gè)元素都可以用數(shù)組索引來(lái)尋址。long和double型的占用2個(gè)單元的本地變量,且用低位元素的索引尋址。例如,對(duì)一個(gè)占用2單元和3單元的double數(shù)值,會(huì)用索引2來(lái)引用。
有一些操作碼可以把int和float型本地變量壓入操作數(shù)棧。部分操作碼,定義成隱含常用本地變量地址的引用。例如,iload_0加載處在位置0的int型本地變量。其他本地變量,通過(guò)操作碼后跟一個(gè)字節(jié)的本地變量索引的方式壓入棧中。iload指令就是這種操作碼類型的一個(gè)例子。iload后的一個(gè)字節(jié)被解釋成指向本地變量的8位無(wú)符號(hào)索引。
類似iload所用的8位無(wú)符號(hào)本地變量索引,限制了一個(gè)方法最多只能有256個(gè)本地變量。有一個(gè)單獨(dú)的wide指令可以把8位索引擴(kuò)展為16位索引,則使得本地變量數(shù)的上限提高到64k個(gè)。操作碼wide只有1個(gè)操作數(shù)。wide和它的操作數(shù),出現(xiàn)在像iload之類的有一個(gè)8位無(wú)符號(hào)本地變量索引的指令之前。JVM會(huì)把wide的操作數(shù)和iload的操作數(shù)合并為一個(gè)16位的無(wú)符號(hào)本地變量索引。
下表列出了把int和float型本地變量壓入棧中的操作碼:
操作碼
|
操作數(shù)
|
描述
|
iload |
vindex |
pushes int from local variable position vindex |
iload_0 |
(none) |
pushes int from local variable position zero |
iload_1 |
(none) |
pushes int from local variable position one |
iload_2 |
(none) |
pushes int from local variable position two |
iload_3 |
(none) |
pushes int from local variable position three |
fload |
vindex |
pushes float from local variable position vindex |
fload_0 |
(none) |
pushes float from local variable position zero |
fload_1 |
(none) |
pushes float from local variable position one |
fload_2 |
(none) |
pushes float from local variable position two |
fload_3 |
(none) |
pushes float from local variable position three |
接下來(lái)的這張表,列出了把long和double型本地變量壓入棧中的指令。這些指令把64位的數(shù)從棧幀的本地變量去移動(dòng)到操作數(shù)區(qū)。
操作碼
|
操作數(shù)
|
描述
|
lload |
vindex |
pushes long from local variable positions vindex and (vindex + 1) |
lload_0 |
(none) |
pushes long from local variable positions zero and one |
lload_1 |
(none) |
pushes long from local variable positions one and two |
lload_2 |
(none) |
pushes long from local variable positions two and three |
lload_3 |
(none) |
pushes long from local variable positions three and four |
dload |
vindex |
pushes double from local variable positions vindex and (vindex + 1) |
dload_0 |
(none) |
pushes double from local variable positions zero and one |
dload_1 |
(none) |
pushes double from local variable positions one and two |
dload_2 |
(none) |
pushes double from local variable positions two and three |
dload_3 |
(none) |
pushes double from local variable positions three and four |
最后一組操作碼,把32位的對(duì)象引用從棧幀的本地變量區(qū)移動(dòng)到操作數(shù)區(qū)。如下表:
操作碼
|
操作數(shù)
|
描述
|
aload |
vindex |
pushes object reference from local variable position vindex |
aload_0 |
(none) |
pushes object reference from local variable position zero |
aload_1 |
(none) |
pushes object reference from local variable position one |
aload_2 |
(none) |
pushes object reference from local variable position two |
aload_3 |
(none) |
pushes object reference from local variable position three |
彈出到本地變量
每一個(gè)將局部變量壓入棧中的操作碼,都有一個(gè)對(duì)應(yīng)的負(fù)責(zé)彈出棧頂元素到本地變量中的操作碼。這些操作碼的名字可以通過(guò)替換入棧操作碼名中的“load”為“store”得到。下表列出了將int和float型數(shù)值彈出操作數(shù)棧到本地變量中的操作碼。這些操作碼將一個(gè)32位的值從棧頂移動(dòng)到本地變量中。
操作碼
|
操作數(shù)
|
描述
|
istore |
vindex |
pops int to local variable position vindex |
istore_0 |
(none) |
pops int to local variable position zero |
istore_1 |
(none) |
pops int to local variable position one |
istore_2 |
(none) |
pops int to local variable position two |
istore_3 |
(none) |
pops int to local variable position three |
fstore |
vindex |
pops float to local variable position vindex |
fstore_0 |
(none) |
pops float to local variable position zero |
fstore_1 |
(none) |
pops float to local variable position one |
fstore_2 |
(none) |
pops float to local variable position two |
fstore_3 |
(none) |
pops float to local variable position three |
下一張表中,展示了負(fù)責(zé)將long和double類型數(shù)值出棧并存到局部變量的字節(jié)碼指令,這些指令將64位的值從操作數(shù)棧頂移動(dòng)到本地變量中。
操作碼
|
操作數(shù)
|
描述
|
lstore |
vindex |
pops long to local variable positions vindex and (vindex + 1) |
lstore_0 |
(none) |
pops long to local variable positions zero and one |
lstore_1 |
(none) |
pops long to local variable positions one and two |
lstore_2 |
(none) |
pops long to local variable positions two and three |
lstore_3 |
(none) |
pops long to local variable positions three and four |
dstore |
vindex |
pops double to local variable positions vindex and (vindex + 1) |
dstore_0 |
(none) |
pops double to local variable positions zero and one |
dstore_1 |
(none) |
pops double to local variable positions one and two |
dstore_2 |
(none) |
pops double to local variable positions two and three |
dstore_3 |
(none) |
pops double to local variable positions three and four |
最后一組操作碼,負(fù)責(zé)將32位的對(duì)象引用從操作數(shù)棧頂移動(dòng)到本地變量中。
操作碼
|
操作數(shù)
|
描述
|
astore |
vindex |
pops object reference to local variable position vindex |
astore_0 |
(none) |
pops object reference to local variable position zero |
astore_1 |
(none) |
pops object reference to local variable position one |
astore_2 |
(none) |
pops object reference to local variable position two |
astore_3 |
(none) |
pops object reference to local variable position three |
類型轉(zhuǎn)換
JVM中有一些操作碼用來(lái)將一種基本類型的數(shù)值轉(zhuǎn)換成另外一種。字節(jié)碼流中的轉(zhuǎn)換操作碼后面不跟操作數(shù),被轉(zhuǎn)換的值取自棧頂。JVM彈出棧頂?shù)闹担D(zhuǎn)換后再將結(jié)果壓入棧中。下表列出了在int,long,float和double間轉(zhuǎn)換的操作碼。這四種類型組合的每一個(gè)可能的轉(zhuǎn)換,都有一個(gè)對(duì)應(yīng)的操作碼。
操作碼
|
操作數(shù)
|
描述
|
i2l |
(none) |
converts int to long |
i2f |
(none) |
converts int to float |
i2d |
(none) |
converts int to double |
l2i |
(none) |
converts long to int |
l2f |
(none) |
converts long to float |
l2d |
(none) |
converts long to double |
f2i |
(none) |
converts float to int |
f2l |
(none) |
converts float to long |
f2d |
(none) |
converts float to double |
d2i |
(none) |
converts double to int |
d2l |
(none) |
converts double to long |
d2f |
(none) |
converts double to float |
下表列出了將int型轉(zhuǎn)換為更小類型的操作碼。不存在直接將long,float,double型轉(zhuǎn)換為比int型小的類型的操作碼。因此,像float到byte這樣的轉(zhuǎn)換,需要兩步。第一步,f2i將float轉(zhuǎn)換為int,第二步,int2byte操作碼將int轉(zhuǎn)換為byte。
操作碼
|
操作數(shù)
|
描述
|
int2byte |
(none) |
converts int to byte |
int2char |
(none) |
converts int to char |
int2short |
(none) |
converts int to short |
雖然存在將int轉(zhuǎn)換為更小類型(byte,short,char)的操作碼,但是不存在反向轉(zhuǎn)換的操作碼。這是因?yàn)閎yte,short和char型的數(shù)值在入棧之前會(huì)轉(zhuǎn)換成int型。byte,short和char型數(shù)值的算術(shù)運(yùn)算,首先要將這些類型的值轉(zhuǎn)為int,然后執(zhí)行算術(shù)運(yùn)算,最后得到int型結(jié)果。也就是說(shuō),如果兩個(gè)byte型的數(shù)相加,會(huì)得到一個(gè)int型的結(jié)果,如果你想要byte型的結(jié)果,你必須顯式地將int類型的結(jié)果轉(zhuǎn)換為byte類型的值。例如,下面的代碼編譯出錯(cuò):
-
class BadArithmetic {
-
byte addOneAndOne() {
-
byte a = 1;
-
byte b = 1;
-
byte c = a + b;
-
return c;
-
}
-
}
javac會(huì)對(duì)上面的代碼給出如下錯(cuò)誤:
-
BadArithmetic.java(7): Incompatible type for declaration.
-
Explicit cast needed to convert int to byte.
-
byte c = a + b;
-
^
Java程序員必須顯式的把a(bǔ) + b的結(jié)果轉(zhuǎn)換為byte,這樣才能通過(guò)編譯。
-
class GoodArithmetic {
-
byte addOneAndOne() {
-
byte a = 1;
-
byte b = 1;
-
byte c = (byte) (a + b);
-
return c;
-
}
-
}
這樣,javac會(huì)很高興的生成GoodArithmetic.class文件,它包含如下的addOneAndOne()方法的字節(jié)碼序列:
-
iconst_1 // Push int constant 1.
-
istore_1 // Pop into local variable 1, which is a: byte a = 1;
-
iconst_1 // Push int constant 1 again.
-
istore_2 // Pop into local variable 2, which is b: byte b = 1;
-
iload_1 // Push a (a is already stored as an int in local variable 1).
-
iload_2 // Push b (b is already stored as an int in local variable 2).
-
iadd // Perform addition. Top of stack is now (a + b), an int.
-
int2byte // Convert int result to byte (result still occupies 32 bits).
-
istore_3 // Pop into local variable 3, which is byte c: byte c = (byte) (a + b);
-
iload_3 // Push the value of c so it can be returned.
-
ireturn // Proudly return the result of the addition: return c;
本文譯自:Bytecode basics