接上文:Java中的類文件結構之二:分析一個.class文件的文本化閱讀
https://blog.csdn.net/kcstrong/article/details/81233672
在上一篇Blog中找一個示例講述瞭如何分析JVM所提供的文本形態說明的.class文件,其中各方法的字節碼部分(表述該方法的實現)並沒有說清楚,本章中希望說清楚各邏輯,但考慮到如果不講一下字節碼的詳細語法,即使分析清楚了上一篇中的示例,也是管中窺豹,只見一斑。因而在分析示例前,先說一下字節碼的詳細語法,若希望直接看示例的字節碼含義部分,可以直接看下一篇的講述。
Java的字節碼語法相對於Java語言,類假於彙編之對於C/C++語言。一般來講,Java語言的開發者是不需要了解的,不瞭解字節碼,也能寫出Java語法,但不一定能百分之百用好Java語言。在工程上,尤其是當前互聯網行業競爭相當激烈的環境,有時候九十分是不令人滿意,一百分才行,做爲有追求的Java程序猿,我們需要熟練的掌握字節碼語法,這樣才能在系統調優,疑難問題排查方面做好。
以下從幾個方面來講一下Java的字節碼:
第一部分講一下字節碼都有什麼,怎麼樣分的類別
第二部分講一下棧結構的一些基本概念,涉及虛擬機棧、線程、棧楨、操作數棧、局部變量表等
第三部分講一下class二進制文件怎麼得到文本形態的字節符描述
第四部分講一下由Java類編譯出的class中方法的字節碼如何工作,以示例說明
一、字節碼列表如下所示:
字節碼 | 助記符 | 指令含義 |
0x00 | nop | None |
0x01 | aconst_null | 將null推送至棧頂 |
0x02 | iconst_m1 | 將int型-1推送至棧頂 |
0x03 | iconst_0 | 將int型0推送至棧頂 |
0x04 | iconst_1 | 將int型1推送至棧頂 |
0x05 | iconst_2 | 將int型2推送至棧頂 |
0x06 | iconst_3 | 將int型3推送至棧頂 |
0x07 | iconst_4 | 將int型4推送至棧頂 |
0x08 | iconst_5 | 將int型5推送至棧頂 |
0x09 | lconst_0 | 將long型0推送至棧頂 |
0x0a | lconst_1 | 將long型1推送至棧頂 |
0x0b | fconst_0 | 將float型0推送至棧頂 |
0x0c | fconst_1 | 將float型1推送至棧頂 |
0x0d | fconst_2 | 將float型2推送至棧頂 |
0x0e | dconst_0 | 將double型0推送至棧頂 |
0x0f | dconst_1 | 將double型1推送至棧頂 |
0x10 | bipush | 將單字節的常量值(-128~127)推送至棧頂 |
0x11 | sipush | 將一個短整型常量(-32768~32767)推送至棧頂 |
0x12 | ldc | 將int,float或String型常量值從常量池中推送至棧頂 |
0x13 | ldc_w | 將int,float或String型常量值從常量池中推送至棧頂(寬索引) |
0x14 | ldc2_w | 將long或double型常量值從常量池中推送至棧頂(寬索引) |
0x15 | iload | 將指定的int型本地變量推送至棧頂 |
0x16 | lload | 將指定的long型本地變量推送至棧頂 |
0x17 | fload | 將指定的float型本地變量推送至棧頂 |
0x18 | dload | 將指定的double型本地變量推送至棧頂 |
0x19 | aload | 將指定的引用類型本地變量推送至棧頂 |
0x1a | iload_0 | 將第一個int型本地變量推送至棧頂 |
0x1b | iload_1 | 將第二個int型本地變量推送至棧頂 |
0x1c | iload_2 | 將第三個int型本地變量推送至棧頂 |
0x1d | iload_3 | 將第四個int型本地變量推送至棧頂 |
0x1e | lload_0 | 將第一個long型本地變量推送至棧頂 |
0x1f | lload_1 | 將第二個long型本地變量推送至棧頂 |
0x20 | lload_2 | 將第三個long型本地變量推送至棧頂 |
0x21 | lload_3 | 將第四個long型本地變量推送至棧頂 |
0x22 | fload_0 | 將第一個float型本地變量推送至棧頂 |
0x23 | fload_1 | 將第二個float型本地變量推送至棧頂 |
0x24 | fload_2 | 將第三個float型本地變量推送至棧頂 |
0x25 | fload_3 | 將第四個float型本地變量推送至棧頂 |
0x26 | dload_0 | 將第一個double型本地變量推送至棧頂 |
0x27 | dload_1 | 將第二個double型本地變量推送至棧頂 |
0x28 | dload_2 | 將第三個double型本地變量推送至棧頂 |
0x29 | dload_3 | 將第四個double型本地變量推送至棧頂 |
0x2a | aload_0 | 將第一個引用類型本地變量推送至棧頂 |
0x2b | aload_1 | 將第二個引用類型本地變量推送至棧頂 |
0x2c | aload_2 | 將第三個引用類型本地變量推送至棧頂 |
0x2d | aload_3 | 將第四個引用類型本地變量推送至棧頂 |
0x2e | iaload | 將int型數組指定索引的值推送至棧頂 |
0x2f | laload | 將long型數組指定索引的值推送至棧頂 |
0x30 | faload | 將float型數組指定索引的值推送至棧頂 |
0x31 | daload | 將double型數組指定索引的值推送至棧頂 |
0x32 | aaload | 將引用類型數組指定索引的值推送至棧頂 |
0x33 | baload | 將boolean或byte型數組指定索引的值推送至棧頂 |
0x34 | caload | 將char型數組指定索引的值推送至棧頂 |
0x35 | saload | 將short型數組指定索引的值推送至棧頂 |
0x36 | istore | 將棧頂int型數值存入指定本地變量 |
0x37 | lstore | 將棧頂long型數值存入指定本地變量 |
0x38 | fstore | 將棧頂float型數值存入指定本地變量 |
0x39 | dstore | 將棧頂double型數值存入指定本地變量 |
0x3a | astore | 將棧頂引用類型數值存入指定本地變量 |
0x3b | istore_0 | 將棧頂int型數值存入第一個本地變量 |
0x3c | istore_1 | 將棧頂int型數值存入第二個本地變量 |
0x3d | istore_2 | 將棧頂int型數值存入第三個本地變量 |
0x3e | istore_3 | 將棧頂int型數值存入第四個本地變量 |
0x3f | lstore_0 | 將棧頂long型數值存入第一個本地變量 |
0x40 | lstore_1 | 將棧頂long型數值存入第二個本地變量 |
0x41 | lstore_2 | 將棧頂long型數值存入第三個本地變量 |
0x42 | lstore_3 | 將棧頂long型數值存入第四個本地變量 |
0x43 | fstore_0 | 將棧頂float型數值存入第一個本地變量 |
0x44 | fstore_1 | 將棧頂float型數值存入第二個本地變量 |
0x45 | fstore_2 | 將棧頂float型數值存入第三個本地變量 |
0x46 | fstore_3 | 將棧頂float型數值存入第四個本地變量 |
0x47 | dstore_0 | 將棧頂double型數值存入第一個本地變量 |
0x48 | dstore_1 | 將棧頂double型數值存入第二個本地變量 |
0x49 | dstore_2 | 將棧頂double型數值存入第三個本地變量 |
0x4a | dstore_3 | 將棧頂double型數值存入第四個本地變量 |
0x4b | astore_0 | 將棧頂引用型數值存入第一個本地變量 |
0x4c | astore_1 | 將棧頂引用型數值存入第二個本地變量 |
0x4d | astore_2 | 將棧頂引用型數值存入第三個本地變量 |
0x4e | astore_3 | 將棧頂引用型數值存入第四個本地變量 |
0x4f | iastore | 將棧頂int型數值存入指定數組的指定索引位置 |
0x50 | lastore | 將棧頂long型數值存入指定數組的指定索引位置 |
0x51 | fastore | 將棧頂float型數值存入指定數組的指定索引位置 |
0x52 | dastore | 將棧頂double型數值存入指定數組的指定索引位置 |
0x53 | aastore | 將棧頂引用型數值存入指定數組的指定索引位置 |
0x54 | bastore | 將棧頂boolean或byte型數值存入指定數組的指定索引位置 |
0x55 | castore | 將棧頂char型數值存入指定數組的指定索引位置 |
0x56 | sastore | 將棧頂short型數值存入指定數組的指定索引位置 |
0x57 | pop | 將棧頂數值彈出(數值不能是long或double類型的) |
0x58 | pop2 | 將棧頂的一個(對於非long或double類型)或兩個數值(對於非long或double的其他類型)彈出 |
0x59 | dup | 複製棧頂數值並將複製值壓入棧頂 |
0x5a | dup_x1 | 複製棧頂數值並將兩個複製值壓入棧頂 |
0x5b | dup_x2 | 複製棧頂數值並將三個(或兩個)複製值壓入棧頂 |
0x5c | dup2 | 複製棧頂一個(對於long或double類型)或兩個(對於非long或double的其他類型)數值並將複製值壓入棧頂 |
0x5d | dup2_x1 | dup_x1指令的雙倍版本 |
0x5e | dup2_x2 | dup_x2指令的雙倍版本 |
0x5f | swap | 將棧頂最頂端的兩個數值互換(數值不能是long或double類型) |
0x60 | iadd | 將棧頂兩int型數值相加並將結果壓入棧頂 |
0x61 | ladd | 將棧頂兩long型數值相加並將結果壓入棧頂 |
0x62 | fadd | 將棧頂兩float型數值相加並將結果壓入棧頂 |
0x63 | dadd | 將棧頂兩double型數值相加並將結果壓入棧頂 |
0x64 | isub | 將棧頂兩int型數值相減並將結果壓入棧頂 |
0x65 | lsub | 將棧頂兩long型數值相減並將結果壓入棧頂 |
0x66 | fsub | 將棧頂兩float型數值相減並將結果壓入棧頂 |
0x67 | dsub | 將棧頂兩double型數值相減並將結果壓入棧頂 |
0x68 | imul | 將棧頂兩int型數值相乘並將結果壓入棧頂 |
0x69 | lmul | 將棧頂兩long型數值相乘並將結果壓入棧頂 |
0x6a | fmul | 將棧頂兩float型數值相乘並將結果壓入棧頂 |
0x6b | dmul | 將棧頂兩double型數值相乘並將結果壓入棧頂 |
0x6c | idiv | 將棧頂兩int型數值相除並將結果壓入棧頂 |
0x6d | ldiv | 將棧頂兩long型數值相除並將結果壓入棧頂 |
0x6e | fdiv | 將棧頂兩float型數值相除並將結果壓入棧頂 |
0x6f | ddiv | 將棧頂兩double型數值相除並將結果壓入棧頂 |
0x70 | irem | 將棧頂兩int型數值作取模運算並將結果壓入棧頂 |
0x71 | lrem | 將棧頂兩long型數值作取模運算並將結果壓入棧頂 |
0x72 | frem | 將棧頂兩float型數值作取模運算並將結果壓入棧頂 |
0x73 | drem | 將棧頂兩double型數值作取模運算並將結果壓入棧頂 |
0x74 | ineg | 將棧頂int型數值取負並將結果壓入棧頂 |
0x75 | lneg | 將棧頂long型數值取負並將結果壓入棧頂 |
0x76 | fneg | 將棧頂float型數值取負並將結果壓入棧頂 |
0x77 | dneg | 將棧頂double型數值取負並將結果壓入棧頂 |
0x78 | ishl | 將int型數值左移指定位數並將結果壓入棧頂 |
0x79 | lshl | 將long型數值左移指定位數並將結果壓入棧頂 |
0x7a | ishr | 將int型數值右(帶符號)移指定位數並將結果壓入棧頂 |
0x7b | lshr | 將long型數值右(帶符號)移指定位數並將結果壓入棧頂 |
0x7c | iushr | 將int型數值右(無符號)移指定位數並將結果壓入棧頂 |
0x7d | lushr | 將long型數值右(無符號)移指定位數並將結果壓入棧頂 |
0x7e | iand | 將棧頂兩int型數值"按位與"並將結果壓入棧頂 |
0x7f | land | 將棧頂兩long型數值"按位與"並將結果壓入棧頂 |
0x80 | ior | 將棧頂兩int型數值"按位或"並將結果壓入棧頂 |
0x81 | lor | 將棧頂兩long型數值"按位或"並將結果壓入棧頂 |
0x82 | ixor | 將棧頂兩int型數值"按位異或"並將結果壓入棧頂 |
0x83 | lxor | 將棧頂兩long型數值"按位異或"並將結果壓入棧頂 |
0x84 | iinc | 將指定int型變量增加指定值(如i++, i--, i+=2等) |
0x85 | i2l | 將棧頂int型數值強制轉換爲long型數值並將結果壓入棧頂 |
0x86 | i2f | 將棧頂int型數值強制轉換爲float型數值並將結果壓入棧頂 |
0x87 | i2d | 將棧頂int型數值強制轉換爲double型數值並將結果壓入棧頂 |
0x88 | l2i | 將棧頂long型數值強制轉換爲int型數值並將結果壓入棧頂 |
0x89 | l2f | 將棧頂long型數值強制轉換爲float型數值並將結果壓入棧頂 |
0x8a | l2d | 將棧頂long型數值強制轉換爲double型數值並將結果壓入棧頂 |
0x8b | f2i | 將棧頂float型數值強制轉換爲int型數值並將結果壓入棧頂 |
0x8c | f2l | 將棧頂float型數值強制轉換爲long型數值並將結果壓入棧頂 |
0x8d | f2d | 將棧頂float型數值強制轉換爲double型數值並將結果壓入棧頂 |
0x8e | d2i | 將棧頂double型數值強制轉換爲int型數值並將結果壓入棧頂 |
0x8f | d2l | 將棧頂double型數值強制轉換爲long型數值並將結果壓入棧頂 |
0x90 | d2f | 將棧頂double型數值強制轉換爲float型數值並將結果壓入棧頂 |
0x91 | i2b | 將棧頂int型數值強制轉換爲byte型數值並將結果壓入棧頂 |
0x92 | i2c | 將棧頂int型數值強制轉換爲char型數值並將結果壓入棧頂 |
0x93 | i2s | 將棧頂int型數值強制轉換爲short型數值並將結果壓入棧頂 |
0x94 | lcmp | 比較棧頂兩long型數值大小, 並將結果(1, 0或-1)壓入棧頂 |
0x95 | fcmpl | 比較棧頂兩float型數值大小, 並將結果(1, 0或-1)壓入棧頂; 當其中一個數值爲NaN 時, 將-1壓入棧頂 |
0x96 | fcmpg | 比較棧頂兩float型數值大小, 並將結果(1, 0或-1)壓入棧頂; 當其中一個數值爲NaN 時, 將1壓入棧頂 |
0x97 | dcmpl | 比較棧頂兩double型數值大小, 並將結果(1, 0或-1)壓入棧頂; 當其中一個數值爲NaN 時, 將-1壓入棧頂 |
0x98 | dcmpg | 比較棧頂兩double型數值大小, 並將結果(1, 0或-1)壓入棧頂; 當其中一個數值爲NaN 時, 將1壓入棧頂 |
0x99 | ifeq | 當棧頂int型數值等於0時跳轉 |
0x9a | ifne | 當棧頂int型數值不等於0時跳轉 |
0x9b | iflt | 當棧頂int型數值小於0時跳轉 |
0x9c | ifge | 當棧頂int型數值大於等於0時跳轉 |
0x9d | ifgt | 當棧頂int型數值大於0時跳轉 |
0x9e | ifle | 當棧頂int型數值小於等於0時跳轉 |
0x9f | if_icmpeq | 比較棧頂兩int型數值大小, 當結果等於0時跳轉 |
0xa0 | if_icmpne | 比較棧頂兩int型數值大小, 當結果不等於0時跳轉 |
0xa1 | if_icmplt | 比較棧頂兩int型數值大小, 當結果小於0時跳轉 |
0xa2 | if_icmpge | 比較棧頂兩int型數值大小, 當結果大於等於0時跳轉 |
0xa3 | if_icmpgt | 比較棧頂兩int型數值大小, 當結果大於0時跳轉 |
0xa4 | if_icmple | 比較棧頂兩int型數值大小, 當結果小於等於0時跳轉 |
0xa5 | if_acmpeq | 比較棧頂兩引用型數值, 當結果相等時跳轉 |
0xa6 | if_acmpne | 比較棧頂兩引用型數值, 當結果不相等時跳轉 |
0xa7 | goto | 無條件跳轉 |
0xa8 | jsr | 跳轉至指定的16位offset位置, 並將jsr的下一條指令地址壓入棧頂 |
0xa9 | ret | 返回至本地變量指定的index的指令位置(一般與jsr或jsr_w聯合使用) |
0xaa | tableswitch | 用於switch條件跳轉, case值連續(可變長度指令) |
0xab | lookupswitch | 用於switch條件跳轉, case值不連續(可變長度指令) |
0xac | ireturn | 從當前方法返回int |
0xad | lreturn | 從當前方法返回long |
0xae | freturn | 從當前方法返回float |
0xaf | dreturn | 從當前方法返回double |
0xb0 | areturn | 從當前方法返回對象引用 |
0xb1 | return | 從當前方法返回void |
0xb2 | getstatic | 獲取指定類的靜態域, 並將其壓入棧頂 |
0xb3 | putstatic | 爲指定類的靜態域賦值 |
0xb4 | getfield | 獲取指定類的實例域, 並將其壓入棧頂 |
0xb5 | putfield | 爲指定類的實例域賦值 |
0xb6 | invokevirtual | 調用實例方法 |
0xb7 | invokespecial | 調用超類構建方法, 實例初始化方法, 私有方法 |
0xb8 | invokestatic | 調用靜態方法 |
0xb9 | invokeinterface | 調用接口方法 |
0xba | invokedynamic | 調用動態方法 |
0xbb | new | 創建一個對象, 並將其引用引用值壓入棧頂 |
0xbc | newarray | 創建一個指定的原始類型(如int, float, char等)的數組, 並將其引用值壓入棧頂 |
0xbd | anewarray | 創建一個引用型(如類, 接口, 數組)的數組, 並將其引用值壓入棧頂 |
0xbe | arraylength | 獲取數組的長度值並壓入棧頂 |
0xbf | athrow | 將棧頂的異常拋出 |
0xc0 | checkcast | 檢驗類型轉換, 檢驗未通過將拋出 ClassCastException |
0xc1 | instanceof | 檢驗對象是否是指定類的實際, 如果是將1壓入棧頂, 否則將0壓入棧頂 |
0xc2 | monitorenter | 獲得對象的鎖, 用於同步方法或同步塊 |
0xc3 | monitorexit | 釋放對象的鎖, 用於同步方法或同步塊 |
0xc4 | wide | 擴展本地變量的寬度 |
0xc5 | multianewarray | 創建指定類型和指定維度的多維數組(執行該指令時, 操作棧中必須包含各維度的長度值), 並將其引用壓入棧頂 |
0xc6 | ifnull | 爲null時跳轉 |
0xc7 | ifnonnull | 不爲null時跳轉 |
0xc8 | goto_w | 無條件跳轉(寬索引) |
0xc9 | jsr_w | 跳轉至指定的32位offset位置, 並將jsr_w的下一條指令地址壓入棧頂 |
字節碼操作符後面會有一個或0個操作數,操作數可能的數據類型包括:l代表long,s代表short、i代表int、b代表byte、c代表char、f代表float、d代表double、a代表reference。以下提到了幾個存儲區:
局部變量表:在方法解析爲字節碼時就已定好的,存儲所用到的局部變量的地方
操作數棧:一個先進後出的棧,用於執行字節碼操作,入棧的可能是操作符,可能是操作數
常量:位於常量池中
便與記憶,可以將操作符分爲以下幾類:(源於《深入理解Java虛擬機》)
1.加載與存儲指令
將一個數值從局部變量表中加載至操作數棧,如:iload(iload_0 == iload 0)
將一個數值從操作數棧存入局部變量表,如:istore(istore_0 == istore 0)
將一個常量加載到操作數棧,如:bipush
擴充局部變量表的訪問索引,如:wide
2.運算指令
加減乘除,例:iadd、fdiv等
位運算,例:ior、ixor等
自增指令:iinc,注意,該運算符操作的是局部變量表,與棧無關
比較指令,例:lcmp
3.類型轉換指令,例:i2b(整型int轉byte)、f2i(float轉int)
4.對象的創建與該問指令
創建類實例:new,注意:先創建的是類實例,然後纔是調用構造方法,也就是說new A(),不止一條操作符
創建數組,例:newarray
訪問類字段或實例字段:getfield、putfield、getstatic、putstatic
檢查類型:instanceof、checkcast
其他數組操作
5.管理操作數棧:pop、pop2、dup等、swap
6.控制跳轉:if有關的(ifeq等)、tableswitch、lookupswitch、goto等
7.方法調用:
invokevirual:調用對象的實例方法(除實現的接口方法外)
invokespecial:調用構造方法、私有方法、父類方法
invokestatic:調用類方法
invokeinterface:調用接口方法
invokedynamic:用戶設定的引導方法(一些動態語言特性相關的支持,在JDK1.8引入)
8.異常處理指令:athrow
9.同步指令:monitorenter、monitorexit
二、相關的幾個概念詳述:
1.棧結構與寄存器結構:目前的PC指令集架構的兩種方式,各有優缺點,棧結構平臺相關性低,可以跨平臺運行於不同的硬件系統上,在x86和arm系列可能同樣處理。寄存器結構運行更快,運行相同邏輯所需要的操作數也要更少,比如Android虛擬機對資源相當敏感,就是寄存器的指令集結構。
2.虛擬機棧與線程、棧幀:Java的方法都是運行於線程中的,或者是主線程,或者是工作線程,每一個方法都是一個棧幀。多個方法(棧幀)組成了一個線程內存塊、每個線程的內存放於一個虛擬機棧中,也就是我們常說的棧區(最大的對比區別是堆區),當前線程正在處理的就是位於最上方的棧幀,如下圖所示(圖片來源於:《深入理解Java虛擬機》):
3.操作數棧與局部變量表:均在方法編譯爲字節碼的同時,就定義好了兩者的大小,分別用:max_stack代表操作棧的最大深度,max_locals代表局部變量表的大小,注意,max_locals的單位爲slot。JVM是基於棧的指令結構,各方法的運行就是通過操作棧與局部變量表配合實現的邏輯,在第一部分中可以看到從操作棧到局部變量表雙向的操作。
三、看一下javap是如何由二進制文件得到文本描述的字節碼錶述的,看一個例子:
package com.demo.kcsdemo.java;
import java.util.Random;
public class Temp5Test {
int index;
public int incc(int incval) {
Random random = new Random();
int ri = random.nextInt();
index = index + incval;
return index;
}
}
編譯該類:javac com/demo/kcsdemo/java/Temp5Test.java,二進制輸出如下所示:
生成字節符描述:javap -verbose Temp5Test
Classfile /Users/wangguoqiang/AndroidStudioProjects/KCSDemo/app/src/main/java/com/demo/kcsdemo/java/Temp5Test.class
Last modified 2018-7-31; size 389 bytes
MD5 checksum 03fc35b2dc3f1aa80072f1a37baedd58
Compiled from "Temp5Test.java"
public class com.demo.kcsdemo.java.Temp5Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // java/util/Random
#3 = Methodref #2.#18 // java/util/Random."<init>":()V
#4 = Methodref #2.#20 // java/util/Random.nextInt:()I
#5 = Fieldref #6.#21 // com/demo/kcsdemo/java/Temp5Test.index:I
#6 = Class #22 // com/demo/kcsdemo/java/Temp5Test
#7 = Class #23 // java/lang/Object
#8 = Utf8 index
#9 = Utf8 I
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 incc
#15 = Utf8 (I)I
#16 = Utf8 SourceFile
#17 = Utf8 Temp5Test.java
#18 = NameAndType #10:#11 // "<init>":()V
#19 = Utf8 java/util/Random
#20 = NameAndType #24:#25 // nextInt:()I
#21 = NameAndType #8:#9 // index:I
#22 = Utf8 com/demo/kcsdemo/java/Temp5Test
#23 = Utf8 java/lang/Object
#24 = Utf8 nextInt
#25 = Utf8 ()I
{
int index;
descriptor: I
flags:
public com.demo.kcsdemo.java.Temp5Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
public int incc(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
LineNumberTable:
line 10: 0
line 11: 8
line 12: 13
line 13: 23
}
SourceFile: "Temp5Test.java"
直接看方法incc的部分:
public int incc(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
LineNumberTable:
line 10: 0
line 11: 8
line 12: 13
line 13: 23
stack=3表示最大操作數棧深度爲3,locals=4表示局部變,局部變量量表中有4個值,args_size=2表示有兩個參數,爲什麼是兩個參數呢?在實例方法(歸屬於對象的方法)中,第一個參數默認爲this,即操作該方法的當前對象,然後按先後順序排列其他的參數,因此incc方法有兩個參數,局部變量表中第0位爲this,然後爲方法的參數表,由先到後,在incc中,第1位爲參數incval。
該方法的二進制碼位於如下圖所示,在兩個紅色標識之間由00000120h0d~00000170h0a:
二進制的分析方法在第一篇:Java中的類文件結構之一:如何分析一個.class文件的二進制碼內容
https://blog.csdn.net/kcstrong/article/details/79460262
之中有詳細的描述,不明白的可以看一下。現在簡單的再提練一下二進制位與字節碼的對應關係:
0x0001:public方法,字節碼的如下部分
flags: ACC_PUBLIC
0x000E000F:對應於常量池中的#14和#15,爲該方法的名稱和描述
#14 = Utf8 incc
#15 = Utf8 (I)I
字節碼的如下部分:
public int incc(int);
descriptor: (I)I
0x0001:包含一個屬性表,0x000C:對應於常量池中的#12,字節碼部分:
Code:
0x00000040:屬性表除名稱和長度外還有0x40個字節
0x0003:max_stack爲3,0x0004:max_local爲4,對應於字節碼錶述爲:
stack=3, locals=4,
0x0000001C:字節碼長1*16+12=28位,後面的28位即爲字節碼:
0xBB00...05AC,再看一下字節碼錶述,正好28位(最左側一排即爲操作符的位偏移量):
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
從裏面找個例子說明下吧,比如,第15條操作符
15: getfield #5 // Field index:I
從0xBB開始,第15位偏移的二進字爲0xB4,查找本篇一開始的操作符表,可知,getfield在表中的位置正是b4位
該操作符後面跟隨着操作數,佔兩位(16、17),因爲下一條操作是從18開始的,因而操作數爲0x0005,正是常量池中的#5常量。
字節碼後面的二進制對應直接從屬性表一一查找,即可,不再展開了,下圖爲屬性表的各字段含義:
四、看一下上述例子中的字節碼的執行過程是什麼:
0: new #2 // class java/util/Random
3: dup
4: invokespecial #3 // Method java/util/Random."<init>":()V
7: astore_2
8: aload_2
9: invokevirtual #4 // Method java/util/Random.nextInt:()I
12: istore_3
13: aload_0
14: aload_0
15: getfield #5 // Field index:I
18: iload_1
19: iadd
20: putfield #5 // Field index:I
23: aload_0
24: getfield #5 // Field index:I
27: ireturn
第0行new執行前的棧爲空,局部變量表爲如下所示:
索引 | 變量 | 類型 |
0 | this | 引用a |
1 | incval | 整型i |
2 | ||
3 |
第0~4條操作符爲創建Random對象,通過new創建對象的過程均爲該寫法,調用了Random的類實例及構造方法,初始化爲random變量,執行完畢後局部變量表不變,操作數棧如下:
random |
第7條操作,將操作數棧頂的變量存入局部變量表中,操作完畢後的局部變量表如下所示,棧爲空
索引 | 變量 | 類型 |
0 | this | 引用a |
1 | incval | 整型i |
2 | random | 引用a |
3 |
第8條操作將局部變量表中的第2條變量入棧,執行完畢後操作數棧如下:
random |
第9條操作調用random的nextInt()方法,執行完畢後操作數棧如下所示:
ri |
第12條將棧頂元素入局部變量表第3的位置:
索引 | 變量 | 類型 |
0 | this | 引用a |
1 | incval | 整型i |
2 | random | 引用a |
3 |
ri | 整型i |
第13、14條執行完畢後,棧變爲:
this |
this |
第15條調用this的getfield,得到當前對象的相關域index並放入棧頂,棧變爲:
index |
this |
第18條執行從局部變量表載入incval,棧變爲:
incval |
index |
this |
第19條將棧頂元素出頂相加再入棧,棧變爲:
index+incval的值 |
this |
第20條將值放入index域中,棧中元素均已出棧,棧空
第23、24條爲讀入this,然後再得到index域並放入棧頂,執行完畢後棧爲:
index |
第27條返回該值
本文爲作者原創作品,寫作不易,轉載請註明出處