LetsCoding.cn

          天地之間有桿秤,拿秤砣砸老百姓。

          字節碼基礎:JVM字節碼初探

          歡迎來到“Under The Hood“第三期。前兩期我們分別介紹了JVM的基本結構和功能Java類文件的基本結構,本期的主要內容有:字節碼所操作的原始類型、類型轉換的字節碼,以及操作JVM棧的字節碼。

          字節碼格式

          字節碼是JVM的機器語言。JVM加載類文件時,對類中的每個方法,它都會得到一個字節碼流。這些字節碼流保存在JVM的方法區中。在程序運行過程中,當一個方法被調用時,它的字節碼流就會被執行。根據特定JVM設計者的選擇,它們可以通過解釋的方式,即時編譯(Just-in-time compilation)的方式或其他技術的方式被執行。

          方法的字節碼流就是JVM的指令(instruction)序列。每條指令包含一個單字節的操作碼(opcode)和0個或多個操作數(operand)。操作碼指明要執行的操作。如果JVM在執行操作前,需要更多的信息,這些信息會以0個或多個操作數的方式,緊跟在操作碼的后面。

          每種類型的操作碼都有一個助記符(mnemonic)。類似典型的匯編語言風格,Java字節碼流可以用它們的助記符和緊跟在后面的操作數來表示。例如,下面的字節碼流可以分解成多個助記符的形式。

          1. // 字節碼流: 03 3b 84 00 01 1a 05 68 3b a7 ff f9
          2. // 分解后:
          3. iconst_0      // 03
          4. istore_0      // 3b
          5. iinc 01     // 84 00 01
          6. iload_0       // 1a
          7. iconst_2      // 05
          8. imul          // 68
          9. istore_0      // 3b
          10. 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”。

          1. // Bytecode stream: 17 01 00
          2. // Dissassembly:
          3. 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類型的值。例如,下面的代碼編譯出錯:

          1. class BadArithmetic {
          2.     byte addOneAndOne() {
          3.         byte a = 1;
          4.         byte b = 1;
          5.         byte c = a + b;
          6.         return c;
          7.     }
          8. }

          javac會對上面的代碼給出如下錯誤:

          1. BadArithmetic.java(7): Incompatible type for declaration.
          2. Explicit cast needed to convert int to byte.
          3.                 byte c = a + b;
          4.                      ^

          Java程序員必須顯式的把a + b的結果轉換為byte,這樣才能通過編譯。

          1. class GoodArithmetic {
          2.     byte addOneAndOne() {
          3.         byte a = 1;
          4.         byte b = 1;
          5.         byte c = (byte) (a + b);
          6.         return c;
          7.     }
          8. }

          這樣,javac會很高興的生成GoodArithmetic.class文件,它包含如下的addOneAndOne()方法的字節碼序列:

          1. iconst_1  // Push int constant 1.
          2. istore_1  // Pop into local variable 1, which is a: byte a = 1;
          3. iconst_1  // Push int constant 1 again.
          4. istore_2  // Pop into local variable 2, which is b: byte b = 1;
          5. iload_1   // Push a (a is already stored as an int in local variable 1).
          6. iload_2   // Push b (b is already stored as an int in local variable 2).
          7. iadd      // Perform addition. Top of stack is now (a + b), an int.
          8. int2byte  // Convert int result to byte (result still occupies 32 bits).
          9. istore_3  // Pop into local variable 3, which is byte c: byte c = (byte) (a + b);
          10. iload_3   // Push the value of c so it can be returned.
          11. ireturn   // Proudly return the result of the addition: return c;

          本文譯自:Bytecode basics

          原創文章,轉載請注明: 轉載自碼農合作社
          本文鏈接地址: 字節碼基礎:JVM字節碼初探

          posted on 2014-05-22 02:07 Rolandz 閱讀(5153) 評論(4)  編輯  收藏 所屬分類: 編程實踐

          評論

          # re: 字節碼基礎:JVM字節碼初探 2014-05-22 05:48 萬利鎖業

          期待更新吧  回復  更多評論   

          # re: 字節碼基礎:JVM字節碼初探 2014-05-23 19:48 IT前線

          在學校沒學這些,工作了也沒接觸,學習了,謝謝
          www.itqx.net  回復  更多評論   

          # re: 字節碼基礎:JVM字節碼初探 2014-05-24 22:32 asd

          在學校沒學到  回復  更多評論   

          # re: 字節碼基礎:JVM字節碼初探 2014-05-26 11:52 手機賺錢網-手機賺錢軟件排行,手機賺錢平臺http://www.9izhuanqian.com

          手機賺錢網-手機賺錢軟件排行,手機賺錢平臺 http://www.9izhuanqian.com  回復  更多評論   

          導航

          統計

          留言簿(1)

          隨筆分類(12)

          隨筆檔案(19)

          積分與排名

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 东山县| 黄石市| 五原县| 永昌县| 无为县| 中卫市| 洛阳市| 浮山县| 泽州县| 犍为县| 光泽县| 仪陇县| 临高县| 平陆县| 丰顺县| 泽普县| 微山县| 陆良县| 胶州市| 高雄县| 阳谷县| 眉山市| 宾阳县| 高台县| 高清| 宣化县| 界首市| 仁化县| 申扎县| 贡嘎县| 临高县| 高淳县| 西丰县| 闸北区| 廊坊市| 棋牌| 芒康县| 左权县| 杨浦区| 高唐县| 涪陵区|