Java字节码指令简介
本文是《深入理解Java虛擬機》中第六章的讀書筆記。
1、概述
在Class文件中,Java方法里的方法體,也就是代表著一個Java源碼程序中程序的部分存儲在方法表集合的Code屬性中。存儲在Code屬性中的是字節碼,也就是編譯后的程序。Java虛擬機的指令由兩部分組成,首先是一個字節長度、代表某種含義的數字(即操作碼),在操作碼后面跟著零個或多個代表這個操作所需的參數(即操作數)。由于Java虛擬機采用的是面向操作數棧而不是寄存器的架構,所以大多數指令不包含操作數,只有一個操作碼。
操作碼的長度只有一個字節,這就限制了操作碼的個數不超過256個。同時,Class文件格式放棄了編譯后代碼的操作數長度對齊,這就意味著虛擬機處理那些超過一個字節的數據時,不得不在運行時從字節中重建出具體數據的結構。比如,如果要將一個兩個字節長的無符號整數使用兩個無符號字節存儲起來分別是byte1和byte2,那么就需要這樣構造出原始的無符號整數:
(byte1<<8) | byte2
這樣會在某種程度上導致執行字節碼時損失一些性能。但這樣做也有好處,那就是由于不需要對齊,省去了中間的填充與間隔符號;用一個字節來表示操作碼,也是為了獲得短小的編譯代碼。這樣就盡可能的減少了編譯后的代碼的長度,非常適合網絡傳輸。
如果不考慮異常處理的話,那么Java虛擬機的解釋器可以使用下面的偽代碼作為基本的執行模型來理解:
do{自動計算PC寄存器的值加1;根據PC寄存器的指示位置,從字節碼流中取出操作碼;if(字節碼存在操作數) 從字節碼流中取出操作數;執行操作碼所定義的操作; }while(字節碼長度>0);2、字節碼與數據類型
在Java虛擬機的指令集中,大多數的指令都包含了操作所對應的數據類型信息。比如iload指令表示從局部變量表中加載int型數據到操作數棧中,而fload表示加載float類型的數據。不過,這兩條指令再虛擬機的內部可能是由同一段代碼來實現的,但在class文件中必須有自己的操作碼。
我們已經知道Java指令的長度只有一個字節,這就限制了指令集的大小。如果每個指令都像上面兩個指令那樣包含所有的數據類型,那么就有可能導致指令過多。因此,Java虛擬機的指令集對于特定的操作只提供了有限的類型相關指令去支持它。比如,大多數指令沒有支持整數類型byte、char和short,甚至沒有指令支持boolean類型。
這些指令中都有特殊的字符來表示專門支持的類型:i代表int類型,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表Reference。
這里僅僅介紹一下指令的種類以及作用,并不會過多的介紹各個指令的含義以及使用,需要的話可以查看《Java虛擬機規范(Java SE 7版)》。
3、加載和存儲指令
加載指令用于將局部變量表中的數據傳送到操作數棧中,而存儲指令用于將操作數棧中的結果傳送到局部變量表中。這類指令包括如下幾種:
- 將一個局部變量加載到操作棧,比如iload、iload<n>、fload、fload<n>、lload、lload<n>、dload、dload<n>、aload、aload<n>;
- 將一個數值從操作數棧存儲到局部變量表,比如istore、istore<n>、lstore、lstore<n>、fstore、fstore<n>、dstore、dstore<n>、astore、astore<n>;
- 將一個常量加載到操作數棧,比如bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>;
- 擴充局部變量表的訪問索引的指令:wide;
上面中帶尖括號的指令實際是一組指令。比如iload<n>,代表了iload_1、iload_2和iload_3。這幾組指令是某個帶操作數的指令(比如iload)的特殊形式,它們省略了操作數,不過操作數隱含在指令中。
4、運算指令
運算或算術指令用于對一個或兩個操作數棧上的值進行某種特定的運算,并將結果存入棧頂。大體上可以分為兩種,對整數進行運算的指令和對浮點數進行運算的指令。不過,由于沒有支持byte、short、char和boolean的算術指令,對于這些數據的運算,會把它們轉化為int類型進行運算。指令列出如下:
- 加法指令:iadd、ladd、fadd、dadd;
- 減法指令:isub、lsub、fsub、dsub;
- 乘法指令:imul、lmul、fmul、dmul;
- 除法指令:idiv、ldiv、fdiv、ddiv;
- 求余指令:irem、lrem、frem、drem;
- 取反指令:ineg、lneg、fneg、dneg;
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr;
- 按位或指令:ior、lor;
- 按位與指令:iand、land;
- 按位異或指令:ixor、lxor;
- 局部變量自增指令:iinc;
- 比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp;
5、類型轉換指令
類型轉換指令用來將兩種不同類型進行轉換,這些轉換操作一般用于實現代碼中的顯示類型轉換操作,或者前面提到的字節碼指令集中數據類型相關指令無法與數據類型一一對應的問題。
虛擬機直接支持寬化類型轉換,即小范圍類型向大范圍類型的安全轉換,不需要顯示的轉換指令。
但是處理窄化類型轉換時,必須顯示使用轉換指令來完成,這些指令包括:i2b、i2c、i2s、l2i、f2l、d2i、d2l和d2f。這些指令可能會導致數值的精度丟失。
6、對象創建與訪問指令
雖然類實例和數組都是對象,但是虛擬機創建類對象和數組的指令是不同的。對象創建后,就可以通過對象訪問指令獲取對象實例或者數組實例中的字段或數組元素,指令如下:
- 創建類實例的指令:new;
- 創建數組的指令:newarray、anewarray、multianewarray;
- 訪問類字段和實例字段的指令:getfield、putfield、getstatic、putstatic;
- 把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload;
- 將一個操作數棧的值存儲到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore;
- 取數組長度的指令:arraylength;
- 檢查類實例類型的指令:instanceof、checkcast;
7、操作數棧管理指令
就像操作一個普通的棧一樣,Java虛擬機提供了一些用于直接操作操作數棧的指令,包括:
- 將操作數棧的棧頂一個或兩個元素出棧:pop、pop2;
- 復制棧頂一個或兩個數值并將復制值或雙份的復制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2;
- 將棧頂最頂端的兩個數值互換:swap;
8、控制轉移指令
控制轉移指令可以讓Java虛擬機有條件或無條件的從指定的位置指令而不是控制轉移指令的下一條指令繼續執行,可以理解為控制轉移指令改變了PC寄存器的值。指令如下:
- 條件分支:ifeq、iflt、ifle、ifgt、ifge、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、、if_icmpge、if_acmpeq和if_acmpne;
- 復合條件分支:tableswitch、lookupswitch;
- 無條件分支:goto、goto_w、jsr、jsr_w、ret;
9、方法調用和返回指令
這里僅僅列出5條用于方法調用的指令:
- invokevirtual指令用于調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式;
- invokeinterface指令用于調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用;
- invokespecial指令用于調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法;
- invokestatic指令用于調用類方法(static方法);
- invokedynamic指令用于在運行時動態解析出調用點限定符索引用的方法,并執行方法,前面4條指令的分派邏輯都固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的;
方法調用指令與類型無關,但是方法返回指令是根據返回值的類型區分的,包括ireturn、lreturn、freturn、dreturn和areturn,另外還有一個return指令供聲明為void的方法、實例初始化方法以及類和接口的類初始化方法使用。
10、異常處理指令
在Java程序中顯式拋出異常的操作(throw語句)都是由athrow指令來實現的,除了用throw語句顯式拋出異常外,Java虛擬機規范還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。
而在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來完成的,而是采用異常表來完成的。
11、同步指令
Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支持的。
方法級的同步是隱式的,即不需要通過字節碼指令來控制,它實現在方法調用和返回操作中。虛擬機可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標志得知一個方法是否聲明為同步方法。當方法調用時,調用指令就會去檢查方法的ACC_SYNCHRONIZED訪問標志是否被設置了,如果設置,執行線程就要求持有管程。在方法執行期間,執行線程持有了管程,其他任何線程都無法再獲取到同一個管程。如果一個方法在執行期間發生了異常,并在方法中無法處理次異常,那么這個同步方法所持有的管程將在異常拋出后自動釋放。
同步一段指令集序列通常是由Java語言中的synchronized語句塊表示的,Java虛擬機的指令集中有monitorenter和monitorexit指令來支持synchronized關鍵字的語義。正確實現synchronized關鍵字需要Javac編譯器和Java虛擬機兩者共同協作。編譯器必須保證每個monitorenter指令都有對應的monitorexit指令。
添加公眾號Machairodus,我會不時分享一些平時學到的東西~
總結
以上是生活随笔為你收集整理的Java字节码指令简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LOL中APEZ是出法穿鞋还是CD鞋?
- 下一篇: 早孕孕酮低有没有推荐的药