字節碼基礎:JVM字節碼初探
歡迎來到“Under The Hood“第三期。前兩期我們分別介紹了JVM的基本結構和功能和Java類文件的基本結構,本期的主要內容有:字節碼所操作的原始類型、類型轉換的字節碼,以及操作JVM棧的字節碼。
字節碼格式
字節碼是JVM的機器語言。JVM加載類文件時,對類中的每個方法,它都會得到一個字節碼流。這些字節碼流保存在JVM的方法區中。在程序運行過程中,當一個方法被調用時,它的字節碼流就會被執行。根據特定JVM設計者的選擇,它們可以通過解釋的方式,即時編譯(Just-in-time compilation)的方式或其他技術的方式被執行。
方法的字節碼流就是JVM的指令(instruction)序列。每條指令包含一個單字節的操作碼(opcode)和0個或多個操作數(operand)。操作碼指明要執行的操作。如果JVM在執行操作前,需要更多的信息,這些信息會以0個或多個操作數的方式,緊跟在操作碼的后面。
每種類型的操作碼都有一個助記符(mnemonic)。類似典型的匯編語言風格,Java字節碼流可以用它們的助記符和緊跟在后面的操作數來表示。例如,下面的字節碼流可以分解成多個助記符的形式。
-
// 字節碼流: 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
字節碼指令集被設計的很緊湊。除了處理跳表的2條指令以外,所有的指令都以字節邊界對齊。操作碼的總數很少,一個字節就能搞定。這最小化了JVM加載前,通過網絡傳輸的類文件的大小;也使得JVM可以維持很小的實現。
JVM中,所有的計算都是圍繞棧(stack)而展開的。因為JVM沒有存儲任意數值的寄存器(register),所有的操作數在計算開始之前,都必須先壓入棧中。因此,字節碼指令主要是用來操作棧的。例如,在上面的字節碼序列中,通過iload_0先把本地變量(local variable)入棧,然后用iconst_2把數字2入棧的方式,來計算本地變量乘以2。兩個整數都入棧之后,imul指令有效的從棧中彈出它們,然后做乘法,最后把運算結果壓入棧中。istore_0指令把結果從棧頂彈出,保存回本地變量。JVM被設計成基于棧,而不是寄存器的機器,這使得它在如80486寄存器架構不佳的處理器上,也能被高效的實現。
原始類型(primitive types)
JVM支持7種原始數據類型。Java程序員可以聲明和使用這些數據類型的變量,而Java字節碼,處理這些數據類型。下表列出了這7種原始數據類型:
類型
|
定義
|
---|---|
byte | 單字節有符號二進制補碼整數 |
short | 2字節有符號二進制補碼整數 |
int | 4字節有符號二進制補碼整數 |
long | 8字節有符號二進制補碼整數 |
float | 4字節IEEE 754單精度浮點數 |
double | 8字節IEEE 754雙精度浮點數 |
char | 2字節無符號Unicode字符 |
原始數據類型以操作數的方式出現在字節碼流中。所有長度超過1字節的原始類型,都以大端(big-endian)的方式保存在字節碼流中,這意味著高位字節出現在低位字節之前。例如,為了把常量值256(0×0100)壓入棧中,你可以用sipush操作碼,后跟一個短操作數。短操作數會以“01 00”的方式出現在字節碼流中,因為JVM是大端的。如果JVM是小端(little-endian)的,短操作數將會是“00 01”。
-
// Bytecode stream: 17 01 00
-
// Dissassembly:
-
sipush 256; // 17 01 00
把常量(constants)壓入棧中
很多操作碼都可以把常量壓入棧中。操作碼以3中不同的方式指定入棧的常量值:由操作碼隱式指明,作為操作數跟在操作碼之后,或者從常量池(constant pool)中獲取。
有些操作碼本身就指明了要入棧的數據類型和常量數值。例如,iconst_1告訴JVM把整數1壓入棧中。這種操作碼,是為不同類型而經常入棧的數值而定義的。它們在字節碼流中只占用1個字節,增進了字節碼的執行效率,并減小了字節碼流的大小。下表列出了int型和float型的操作碼:
操作碼
|
操作數
|
描述
|
---|---|---|
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位寬的,因此,每次一個int數和float數入棧,它都占用一個單元。下表列出的操作碼處理long型和double型。long型和double型的數值占用64位。每次一個long數或double數被壓入棧中,它都占用2個棧單元。下面的表格,列出了隱含處理long型和double型的操作碼
操作碼
|
操作數
|
描述
|
---|---|---|
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 |
另外還有一個隱含入棧常量值的操作碼,aconst_null,它把空對象(null object)的引用(reference)壓入棧中。對象引用的格式取決于JVM實現。對象引用指向垃圾收集堆(garbage-collected heap)中的對象??諏ο笠?,意味著一個變量當前沒有指向任何合法對象。aconst_null操作碼用在給引用變量賦null值的時候。
操作碼
|
操作數
|
描述
|
---|---|---|
aconst_null | (none) | pushes a null object reference onto the stack |
有2個操作碼需要緊跟一個操作數來指明入棧的常量值。下表列出的操作碼,用來把合法的byte型和short型的常量值壓入棧中。byte型或short型的值在入棧之前,先被擴展成int型的值,因為棧單元是32位寬的。對byte型和short型的操作,實際上是基于它們擴展后的int型值的。
操作碼
|
操作數
|
描述
|
---|---|---|
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個操作碼把常量池中的常量值壓入棧中。所有和類關聯的常量,如final變量,都被保存在類的常量池中。把常量池中的常量壓入棧中的操作碼,都有一個操作數,它表示需要入棧的常量在常量池中的索引。JVM會根據索引查找常量,確定它的類型,并把它壓入棧中。
在字節碼流中,常量池索引(constant pool index)是一個緊跟在操作碼后的無符號值。操作碼lcd1和lcd2把32位的項壓入棧中,如int或float。兩者的區別在于lcd1只適用于1-255的常量池索引位,因為它的索引只有1個字節。(常量池0號位未被使用。)lcd2的索引有2個字節,所以它可以適用于常量池的任意位置。lcd2w也有一個2字節的索引,它被用來指示任意含有64位的long或double型數據的常量池位置。下表列出了把常量池中的常量壓入棧中的操作碼:
操作碼
|
操作數
|
描述
|
---|---|---|
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)壓入棧中
局部變量保存在棧幀的一個特殊區域中。棧幀是當前執行方法正在使用的棧區。每個棧幀包含3個部分:本地變量區,執行環境和操作數棧區。把本地變量入棧實際上包含了把數值從棧幀的本地變量區移動到操作數棧區。操作數棧區總是在棧的頂部,所以,把一個值壓到當前棧幀的操作數棧區頂部,跟壓到整個JVM棧的頂部是一個意思。
Java棧是一個先進后出(LIFO)的32位寬的棧。所有的本地變量至少占用32位,因為棧中的每個單元都是32位寬的。像long和double類型的64位的本地變量會占用2個棧單元。byte和short型的本地變量會當做int型來存儲,但只擁有較小類型的合法值。例如,表示byte型的int型本地變量取值范圍總是-128到127。
每個本地變量都有一個唯一索引。方法棧幀的本地變量區,可以當成是一個擁有32位寬的元素的數組,每個元素都可以用數組索引來尋址。long和double型的占用2個單元的本地變量,且用低位元素的索引尋址。例如,對一個占用2單元和3單元的double數值,會用索引2來引用。
有一些操作碼可以把int和float型本地變量壓入操作數棧。部分操作碼,定義成隱含常用本地變量地址的引用。例如,iload_0加載處在位置0的int型本地變量。其他本地變量,通過操作碼后跟一個字節的本地變量索引的方式壓入棧中。iload指令就是這種操作碼類型的一個例子。iload后的一個字節被解釋成指向本地變量的8位無符號索引。
類似iload所用的8位無符號本地變量索引,限制了一個方法最多只能有256個本地變量。有一個單獨的wide指令可以把8位索引擴展為16位索引,則使得本地變量數的上限提高到64k個。操作碼wide只有1個操作數。wide和它的操作數,出現在像iload之類的有一個8位無符號本地變量索引的指令之前。JVM會把wide的操作數和iload的操作數合并為一個16位的無符號本地變量索引。
下表列出了把int和float型本地變量壓入棧中的操作碼:
操作碼
|
操作數
|
描述
|
---|---|---|
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 |
接下來的這張表,列出了把long和double型本地變量壓入棧中的指令。這些指令把64位的數從棧幀的本地變量去移動到操作數區。
操作碼
|
操作數
|
描述
|
---|---|---|
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位的對象引用從棧幀的本地變量區移動到操作數區。如下表:
操作碼
|
操作數
|
描述
|
---|---|---|
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 |
彈出到本地變量
每一個將局部變量壓入棧中的操作碼,都有一個對應的負責彈出棧頂元素到本地變量中的操作碼。這些操作碼的名字可以通過替換入棧操作碼名中的“load”為“store”得到。下表列出了將int和float型數值彈出操作數棧到本地變量中的操作碼。這些操作碼將一個32位的值從棧頂移動到本地變量中。
操作碼
|
操作數
|
描述
|
---|---|---|
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 |
下一張表中,展示了負責將long和double類型數值出棧并存到局部變量的字節碼指令,這些指令將64位的值從操作數棧頂移動到本地變量中。
操作碼
|
操作數
|
描述
|
---|---|---|
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 |
最后一組操作碼,負責將32位的對象引用從操作數棧頂移動到本地變量中。
操作碼
|
操作數
|
描述
|
---|---|---|
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 |
類型轉換
JVM中有一些操作碼用來將一種基本類型的數值轉換成另外一種。字節碼流中的轉換操作碼后面不跟操作數,被轉換的值取自棧頂。JVM彈出棧頂的值,轉換后再將結果壓入棧中。下表列出了在int,long,float和double間轉換的操作碼。這四種類型組合的每一個可能的轉換,都有一個對應的操作碼。
操作碼
|
操作數
|
描述
|
---|---|---|
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型轉換為更小類型的操作碼。不存在直接將long,float,double型轉換為比int型小的類型的操作碼。因此,像float到byte這樣的轉換,需要兩步。第一步,f2i將float轉換為int,第二步,int2byte操作碼將int轉換為byte。
操作碼
|
操作數
|
描述
|
---|---|---|
int2byte | (none) | converts int to byte |
int2char | (none) | converts int to char |
int2short | (none) | converts int to short |
雖然存在將int轉換為更小類型(byte,short,char)的操作碼,但是不存在反向轉換的操作碼。這是因為byte,short和char型的數值在入棧之前會轉換成int型。byte,short和char型數值的算術運算,首先要將這些類型的值轉為int,然后執行算術運算,最后得到int型結果。也就是說,如果兩個byte型的數相加,會得到一個int型的結果,如果你想要byte型的結果,你必須顯式地將int類型的結果轉換為byte類型的值。例如,下面的代碼編譯出錯:
-
class BadArithmetic {
-
byte addOneAndOne() {
-
byte a = 1;
-
byte b = 1;
-
byte c = a + b;
-
return c;
-
}
-
}
javac會對上面的代碼給出如下錯誤:
-
BadArithmetic.java(7): Incompatible type for declaration.
-
Explicit cast needed to convert int to byte.
-
byte c = a + b;
-
^
Java程序員必須顯式的把a + b的結果轉換為byte,這樣才能通過編譯。
-
class GoodArithmetic {
-
byte addOneAndOne() {
-
byte a = 1;
-
byte b = 1;
-
byte c = (byte) (a + b);
-
return c;
-
}
-
}
這樣,javac會很高興的生成GoodArithmetic.class文件,它包含如下的addOneAndOne()方法的字節碼序列:
-
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
原創文章,轉載請注明: 轉載自碼農合作社
本文鏈接地址: 字節碼基礎:JVM字節碼初探
posted on 2014-05-22 02:07 Rolandz 閱讀(5153) 評論(4) 編輯 收藏 所屬分類: 編程實踐