虛擬機字節碼指令


Java虛擬機的指令由一個字節長度的、代表着某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其后的零至多個代表此操作所需參數(稱為操作數,Operands)而構成。

基本數據類型

1、除了long和double類型外,每個變量都占局部變量區中的一個變量槽(slot),而long及double會占用兩個連續的變量槽。

2、大多數對於boolean、byte、short和char類型數據的操作,都使用相應的int類型作為運算類型。

 

加載和存儲指令

1、將一個局部變量加載到操作棧:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
2、將一個數值從操作數棧存儲到局部變量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
3、將一個常量加載到操作數棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
4、擴充局部變量表的訪問索引的指令:wide。

_<n>:_0、_1、_2、_3,

存儲數據的操作數棧和局部變量表主要就是由加載和存儲指令進行操作,除此之外,還有少量指令,如訪問對象的字段或數組元素的指令也會向操作數棧傳輸數據。

二、const系列
該系列命令主要負責把簡單的數值類型送到棧頂。該系列命令不帶參數。注意只把簡單的數值類型送到棧頂時,才使用如下的命令。
比如對應int型才該方式只能把-1,0,1,2,3,4,5(分別采用iconst_m1,iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5)
送到棧頂。對於int型,其他的數值請使用push系列命令(比如bipush)。
指令碼    助記符                            說明
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)推送至棧頂
三、push系列
該系列命令負責把一個整形數字(長度比較小)送到到棧頂。該系列命令有一個參數,用於指定要送到棧頂的數字。
注意該系列命令只能操作一定范圍內的整形數值,超出該范圍的使用將使用ldc命令系列。
指令碼    助記符                            說明
0x10          bipush    將單字節的常量值(-128~127)推送至棧頂
0x11           sipush    將一個短整型常量值(-32768~32767)推送至棧頂
四、ldc系列
該系列命令負責把數值常量或String常量值從常量池中推送至棧頂。該命令后面需要給一個表示常量在常量池中位置(編號)的參數,
哪些常量是放在常量池呢?比如:final static int id=32768;final static float double=6.5。
對於const系列命令和push系列命令操作范圍之外的數值類型常量,都放在常量池中.
另外,所有不是通過new創建的String都是放在常量池中的。
指令碼    助記符                               說明
0x12            ldc                 將int, float或String型常量值從常量池中推送至棧頂
0x13          ldc_w               將int, float或String型常量值從常量池中推送至棧頂(寬索引)
0x14          ldc2_w             將long或double型常量值從常量池中推送至棧頂(寬索引)
五、load系列
5.1、load系列A
該系列命令負責把本地變量的送到棧頂。這里的本地變量不僅可以是數值類型,還可以是引用類型。
對於前四個本地變量可以采用iload_0,iload_1,iload_2,iload_3(它們分別表示第0,1,2,3個整形變量)這種不到參數的簡化命令形式。
對於第4以上的本地變量將使用iload命令這種形式,在它后面給一參數,以表示是對第幾個(從0開始)本類型的本地變量進行操作。
對本地變量所進行的編號,是對所有類型的本地變量進行的(並不按照類型分類)。
對於非靜態函數,第一變量是this,即其對於的操作是aload_0.
還有函數傳入參數也算本地變量,在進行編號時,它是先於函數體的本地變量的。
指令碼    助記符                                        說明
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                     將第四個引用類型本地變量推送至棧頂
5.2、load系列B
該系列命令負責把數組的某項送到棧頂。該命令根據棧里內容來確定對哪個數組的哪項進行操作。
比如,如果有成員變量:final String names[]={"robin","hb"};
那么這句話:String str=names[0];對應的指令為
   17: aload_0                                                            //將this引用推送至棧頂,即壓入棧。
   18: getfield #5; //Field names:[Ljava/lang/String;//將棧頂的指定的對象的第5個實例域(Field)的值(這個值可能是引用,這里就是引用)壓入棧頂
   21: iconst_0                                                            //數組的索引值(下標)推至棧頂,即壓入棧
   22: aaload                                                              //根據棧里內容來把name數組的第一項的值推至棧頂
   23: astore 5                                                       //把棧頂的值存到str變量里。因為str在我的程序中是其所在非靜態函數的第5個變量(從0開始計數),
指令碼    助記符                               說明
0x2e         iaload                     將int型數組指定索引的值推送至棧頂
0x2f          laload                     將long型數組指定索引的值推送至棧頂
0x30         faload                     將float型數組指定索引的值推送至棧頂
0x31        daload                     將double型數組指定索引的值推送至棧頂
0x32        aaload                     將引用型數組指定索引的值推送至棧頂
0x33        baload                     將boolean或byte型數組指定索引的值推送至棧頂
0x34        caload                     將char型數組指定索引的值推送至棧頂
0x35        saload                     將short型數組指定索引的值推送至棧頂
六、store系列
6.1、store系列A
該系列命令負責把棧頂的值存入本地變量。這里的本地變量不僅可以是數值類型,還可以是引用類型。
如果是把棧頂的值存入到前四個本地變量的話,采用的是istore_0,istore_1,istore_2,istore_3(它們分別表示第0,1,2,3個本地整形變量)這種不到參數的簡化命令形式。如果是把棧頂的值存入到第四個以上本地變量的話,將使用istore命令這種形式,在它后面給一參數,以表示是把棧頂的值存入到第幾個(從0開始)本地變量中。
對本地變量所進行的編號,是對所有類型的本地變量進行的(並不按照類型分類)。
對於非靜態函數,第一變量是this,它是只讀的.
還有函數傳入參數也算本地變量,在進行編號時,它是先於函數體的本地變量的。
指令碼    助記符                               說明
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                將棧頂引用型數值存入第四個本地變量
6.2、store系列B
該系列命令負責把棧頂項的值存到數組里。該命令根據棧里內容來確定對哪個數組的哪項進行操作。
比如,如下代碼:
int moneys[]=new int[5];
moneys[1]=100;
其對應的指令為:
   49: iconst_5
   50: newarray int
   52: astore 11
   54: aload 11
   56: iconst_1
   57: bipush 100
   59: iastore
   60: lload 6       //因為str在我的程序中是其所非靜態在函數的第6個變量(從0開始計數).
指令碼    助記符                                   說明
0x4f         iastore               將棧頂int型數值存入指定數組的指定索引位置
0x50        lastore               將棧頂long型數值存入指定數組的指定索引位置
0x51        fastore               將棧頂float型數值存入指定數組的指定索引位置
0x52        dastore              將棧頂double型數值存入指定數組的指定索引位置
0x53        aastore              將棧頂引用型數值存入指定數組的指定索引位置
0x54        bastore              將棧頂boolean或byte型數值存入指定數組的指定索引位置
0x55        castore              將棧頂char型數值存入指定數組的指定索引位置
0x56        sastore              將棧頂short型數值存入指定數組的指定索引位置
七、pop系列
該系列命令似乎只是簡單對棧頂進行操作,更多詳情待補充。
指令碼     助記符                                   說明
0x57            pop           將棧頂數值彈出 (數值不能是long或double類型的)
0x58            pop2         將棧頂的一個(long或double類型的)或兩個數值彈出(其它)
0x59            dup           復制棧頂數值(數值不能是long或double類型的)並將復制值壓入棧頂
0x5a            dup_x1     復制棧頂數值(數值不能是long或double類型的)並將兩個復制值壓入棧頂
0x5b            dup_x2     復制棧頂數值(數值不能是long或double類型的)並將三個(或兩個)復制值壓入棧頂
0x5c            dup2         復制棧頂一個(long或double類型的)或兩個(其它)數值並將復制值壓入棧頂
0x5d            dup2_x1    復制棧頂數值(long或double類型的)並將兩個復制值壓入棧頂
0x5e            dup2_x2     復制棧頂數值(long或double類型的)並將三個(或兩個)復制值壓入棧頂
八、棧頂元素數學操作及移位操作系列
該系列命令用於對棧頂元素行數學操作,和對數值進行移位操作。移位操作的操作數和要移位的數都是從棧里取得。
比如對於代碼:int k=100;k=k>>1;其對應的JVM指令為:
   60: bipush 100
   62: istore 12//因為k在我的程序中是其所在非靜態函數的第12個變量(從0開始計數).
   64: iload 12
   66: iconst_1
   67: ishr
   68: istore 12
指令碼     助記符                                        說明
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型數值作“按位異或”並將結果壓入棧頂

 

 

運算指令

1、運算或算術指令用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。

2、算術指令分為兩種:整型運算的指令和浮點型運算的指令。

3、無論是哪種算術指令,都使用Java虛擬機的數據類型,由於沒有直接支持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。

類型轉換指令

1、類型轉換指令可以將兩種不同的數值類型進行相互轉換。

2、這些轉換操作一般用於實現用戶代碼中的顯式類型轉換操作,或者用來處理字節碼指令集中數據類型相關指令無法與數據類型一一對應的問題。

寬化類型轉換

int類型到long、float或者double類型。
long類型到float、double類型。
float類型到double類型。

i2l、f2b、l2f、l2d、f2d。

窄化類型轉換

i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

對象創建與訪問指令

創建類實例的指令:new。
創建數組的指令:newarray、anewarray、multianewarray。
訪問類字段(static字段,或者稱為類變量)和實例字段(非static字段,或者稱為實例變量)的指令:getfield、putfield、getstatic、putstatic。
把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
將一個操作數棧的值存儲到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取數組長度的指令:arraylength。
檢查類實例類型的指令:instanceof、checkcast。

操作數棧管理指令

直接操作操作數棧的指令:

將操作數棧的棧頂一個或兩個元素出棧:pop、pop2。
復制棧頂一個或兩個數值並將復制值或雙份的復制值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
將棧最頂端的兩個數值互換:swap。

控制轉移指令

1、控制轉移指令可以讓Java虛擬機有條件或無條件地從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程序。

2、從概念模型上理解,可以認為控制轉移指令就是在有條件或無條件地修改PC寄存器的值。

條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
復合條件分支:tableswitch、lookupswitch。
無條件分支:goto、goto_w、jsr、jsr_w、ret。

在Java虛擬機中有專門的指令集用來處理int和reference類型的條件分支比較操作,為了可以無須明顯標識一個實體值是否null,也有專門的指令用來檢測null值。

方法調用和返回指令

invokevirtual 指令用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
invokeinterface 指令用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
invokespecial 指令用於調用一些需要特殊處理的實例方法,包括實例初始化(<init>)方法、私有方法和父類方法。
invokestatic  調用靜態方法(static方法)。
invokedynamic 指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法,前面4條調用指令的分派邏輯都固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。

方法調用指令與數據類型無關,而方法返回指令是根據返回值的類型區分的,包括ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供聲明為void的方法、實例初始化方法以及類和接口的類初始化方法使用。

關於方法調用

1、Class文件的編譯過程中不包含傳統編譯中的連接步驟,所有方法調用中的目標方法在Class文件里面都是一個常量池中的符號引用,而不是方法在實際運行時內存布局中的入口地址。

2、在類加載的解析階段,會將其中的一部分符號引用轉化為直接引用,這類方法(編譯期可知,運行期不可變)的調用稱為解析(Resolution)。

主要包括靜態方法和私有方法兩大類,前者與類型直接關聯,后者在外部不可被訪問,這兩種方法各自的特點決定了它們都不可能通過繼承或別的方式重寫其他版本,因此它們都適合在類加載階段進行解析。

3、只要能被invokestatic和invokespecial指令調用的方法,都可以在解析階段中確定唯一的調用版本,符合這個條件的有靜態方法、私有方法、實例構造器、父類方法4類,它們在類加載的時候就會把符號引用解析為該方法的直接引用。

4、動態語言支持

動態類型語言的關鍵特征是它的類型檢查的主體過程是在運行期而不是編譯期。

 

異常處理指令

在Java程序中顯式拋出異常的操作(throw語句)都由athrow指令來實現,除了用throw語句顯式拋出異常情況之外,Java虛擬機規范還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。

例如,在整數運算中,當除數為零時,虛擬機會在idiv或ldiv指令中拋出ArithmeticException異常。

而在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的(很久之前曾經使用jsr和ret指令來實現,現在已經不用了),而是采用異常表來完成的。

同步指令

Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都是使用管程(Monitor)來支持的。

方法級同步

方法級的同步是隱式的,即無須通過字節碼指令來控制

它實現在方法調用和返回操作之中。虛擬機可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標志得知一個方法是否聲明為同步方法。當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否被設置,如果設置了,執行線程就要求先成功持有管程,然后才能執行方法,最后當方法完成(無論是正常完成還是非正常完成)時釋放管程。在方法執行期間,執行線程持有了管程,其他任何線程都無法再獲取到同一個管程。如果一個同步方法執行期間拋出了異常,並且在方法內部無法處理此異常,那么這個同步方法所持有的管程將在異常拋到同步方法之外時自動釋放。

方法內部一段指令序列的同步
同步一段指令集序列通常是由Java語言中的synchronized語句塊來表示的,Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義,正確實現synchronized關鍵字需要Javac編譯器與Java虛擬機兩者共同協作支持。

 

虛擬機運行活化的內存數據中的指令:程序的執行

前面我們說明了java源碼被編譯成為了二進制字節碼,二進制字節碼轉為內存中方法區里存儲的活化對象,那么最重要的程序執行就做好了基礎:當方法區里的字段和方法按照虛擬機規定的數據結構排好,常量池中的符號引用數據在加載過程中最大限度地轉為了直接引用,那么這個時候虛擬機就可以在加載主類后創建新的線程按步執行主類的main函數中的指令了。

java虛擬機執行程序的基礎是特定的二進制指令集和運行時棧幀:

  • 二進制指令集是java虛擬機規定的一些指令,在編譯后二進制字節碼的類方法里的字節碼就是這種指令,所以只要找到方法區里的類方法就可以依照這套指令集去執行命令。

  • 運行時棧幀是虛擬機執行的物理所在,在這個棧幀結構上,方法的局部變量、操作數棧、動態鏈接和返回地址依序排列,依照命令動態變換棧幀上的數據,最終完成所有的這個方法上的指令。

棧幀的進一步划分:

  • 局部變量表:包括方法的參數和方法體內部的局部變量都會存在這個表中。

  • 操作數棧:操作數棧是一個運行中間產生的操作數構成的棧,這個棧的棧頂保存的就是當前活躍的操作數。

  • 動態鏈接:我們之前提到這個方法中調用的方法和類在常量池中的符號引用轉換為的直接引用就保存在這里,只要訪問到這些方法和類的時候就會根據動態鏈接去直接引用所指的地址加載那些方法。

  • 返回地址:程序正常結束恢復上一個棧幀的狀態的時候需要知道上一個指令的地址。

現在我們使用一個綜合實例來說明運行的整個過程:

源代碼如下,邏輯很簡單:

public class TestDemo { public static int minus(int x){ return -x; } public static void main(String[] args) { int x = 5; int y = minus(x); } } 

我們可以分析它的二進制字節碼,當然這里我們借助javap工具進行分析:

jinhaoplus$ javap -verbose TestDemo Classfile /Users/jinhao/Desktop/TestDemo.class Last modified 2015-10-17; size 342 bytes MD5 checksum 4f37459aa1b3438b1608de788d43586d Compiled from "TestDemo.java" public class TestDemo minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Methodref #3.#16 // TestDemo.minus:(I)I #3 = Class #17 // TestDemo #4 = Class #18 // java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 minus #10 = Utf8 (I)I #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 TestDemo.java #15 = NameAndType #5:#6 // "<init>":()V #16 = NameAndType #9:#10 // minus:(I)I #17 = Utf8 TestDemo #18 = Utf8 java/lang/Object { public TestDemo(); 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 1: 0 public static int minus(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: ineg 2: ireturn LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_5 1: istore_1 2: iload_1 3: invokestatic #2 // Method minus:(I)I 6: istore_2 7: return LineNumberTable: line 6: 0 line 7: 2 line 8: 7 } SourceFile: "TestDemo.java" 

這個過程是從固化在class文件中的二進制字節碼開始,經過加載器對當前類的加載,虛擬機對二進制碼的驗證、准備和一定的解析,進入內存中的方法區,常量池中的符號引用一定程度上轉換為直接引用,使得字節碼通過結構化的組織讓虛擬機了解類的每一塊的構成,創建的線程申請到了虛擬機棧中的空間構造出屬於這一線程的棧幀空間,執行主類的main方法:

public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=3, args_size=1 0: iconst_5 1: istore_1 2: iload_1 3: invokestatic #2 // Method minus:(I)I 6: istore_2 7: return LineNumberTable: line 6: 0 line 7: 2 line 8: 7 }

首先檢查main的訪問標志、描述符描述的返回類型和參數列表,確定可以訪問后進入Code屬性表執行命令,讀入棧深度建立符合要求的操作數棧,讀入局部變量大小建立符合要求的局部變量表,根據參數數向局部變量表中依序加入參數(第一個參數是引用當前對象的this,所以空參數列表的參數數也是1),然后開始根據命令正式執行:

0: iconst_5

將整數5壓入棧頂

1: istore_1

將棧頂整數值存入局部變量表的slot1(slot0是參數this)

2: iload_1

將slot1壓入棧頂

3: invokestatic #2 // Method minus:(I)I

二進制invokestatic方法用於調用靜態方法,參數是根據常量池中已經轉換為直接引用的常量,意即minus函數在方法區中的地址,找到這個地址調用函數,向其中加入的參數為棧頂的值

6: istore_2

將棧頂整數存入局部變量的slot2

7: return

將返回地址中存儲的PC地址返到PC,棧幀恢復到調用前

現在我們分析調用minus函數的時候進入minus函數的過程:

public static int minus(int); descriptor: (I)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: iload_0 1: ineg 2: ireturn LineNumberTable: line 3: 0

同樣的首先檢查minus函數的訪問標志、描述符描述的返回類型和參數列表,確定可以訪問后進入Code屬性表執行命令,讀入棧深度建立符合要求的操作數棧,讀入局部變量大小建立符合要求的局部變量表,根據參數數向局部變量表中依序加入參數,然后開始根據命令正式執行:

0: iload_0

將slot0壓入棧頂,也就是傳入的參數

1: ineg

將棧頂的值彈出取負后壓回棧頂

2: ireturn

將返回地址中存儲的PC地址返到PC,棧幀恢復到調用前

這個過程結束后對象的生命周期結束,因此開始執行GC回收內存中的對象,包括堆中的類對應的java.lang.Class對象,卸載方法區中的類。

方法的解析和分派

上面這個例子中main方法里調用minus方法的時候是沒有二義性的,因為從二進制字節碼里我們可以看到invokestatic方法調用的是minus方法的直接引用,也就說在編譯期這個調用就已經決定了。這個時候我們來說說方法調用,這個部分的內容在前面的類加載時候提過,在能夠唯一確定方法的直接引用的時候虛擬機會將常量表里的符號引用轉換為直接引用,這樣在運行的時候就可以直接根據這個地址找到對應的方法去執行,這種時候的轉換才能叫做我們當時提到的在連接過程中的解析。
但是如果方法是動態綁定的,也就是說在編譯期我們並不知道使用哪個方法(或者叫不知道使用方法的哪個版本),那么這個時候就需要在運行時才能確定哪個版本的方法將被調用,這個時候才能將符號引用轉換為直接引用。這個問題提到的多個版本的方法在java中的重載和多態重寫問題息息相關。

重載(override)

public class TestDemo { static class Human{ } static class Man extends Human{ } static class Woman extends Human{ } public void sayHello(Human human) { System.out.println("hello human"); } public void sayHello(Man man) { System.out.println("hello man"); } public void sayHello(Woman woman) { System.out.println("hello woman"); } public static void main(String[] args) { TestDemo demo = new TestDemo(); Human man = new Man(); Human woman = new Woman(); demo.sayHello(man); demo.sayHello(woman); } }

javap結果:

public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: new #7 // class TestDemo 3: dup 4: invokespecial #8 // Method "<init>":()V 7: astore_1 8: new #9 // class TestDemo$Man 11: dup 12: invokespecial #10 // Method TestDemo$Man."<init>":()V 15: astore_2 16: new #11 // class TestDemo$Woman 19: dup 20: invokespecial #12 // Method TestDemo$Woman."<init>":()V 23: astore_3 24: aload_1 25: aload_2 26: invokevirtual #13 // Method sayHello:(LTestDemo$Human;)V 29: aload_1 30: aload_3 31: invokevirtual #13 // Method sayHello:(LTestDemo$Human;)V 34: return LineNumberTable: line 21: 0 line 22: 8 line 23: 16 line 24: 24 line 25: 29 line 26: 34 

重寫(overwrite)

public class TestDemo { static class Human{ public void sayHello() { System.out.println("hello human"); } } static class Man extends Human{ public void sayHello() { System.out.println("hello man"); } } static class Woman extends Human{ public void sayHello() { System.out.println("hello woman"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); } }

javap結果:

public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class TestDemo$Man 3: dup 4: invokespecial #3 // Method TestDemo$Man."<init>":()V 7: astore_1 8: new #4 // class TestDemo$Woman 11: dup 12: invokespecial #5 // Method TestDemo$Woman."<init>":()V 15: astore_2 16: aload_1 17: invokevirtual #6 // Method TestDemo$Human.sayHello:()V 20: aload_2 21: invokevirtual #6 // Method TestDemo$Human.sayHello:()V 24: return LineNumberTable: line 20: 0 line 21: 8 line 22: 16 line 23: 20 line 24: 24 

我們可以看出來無論是重載還是重寫,都是二進制指令invokevirtual調用了sayHello方法來執行的。

  • 在重載中,程序調用的是參數實際類型不同的方法,但是虛擬機最終分派了相同外觀類型(靜態類型)的方法,這說明在重載的過程中虛擬機在運行的時候是只看參數的外觀類型(靜態類型)的,而這個外觀類型(靜態類型)是在編譯的時候就已經確定的,和虛擬機沒有關系。這種依賴靜態類型來做方法的分配叫做靜態分派。

  • 在重寫中,程序調用的是不同實際類型的同名方法,虛擬機依據對象的實際類型去尋找是否有這個方法,如果有就執行,如果沒有去父類里找,最終在實際類型里找到了這個方法,所以最終是在運行期動態分派了方法。在編譯的時候我們可以看到字節碼指示的方法都是一樣的符號引用,但是運行期虛擬機能夠根據實際類型去確定出真正需要的直接引用。這種依賴實際類型來做方法的分配叫做動態分派。得益於java虛擬機的動態分派會在分派前確定對象的實際類型,面向對象的多態性才能體現出來。

對象的創建和堆內存的分配

前面我們提到的都是類在方法區中的內存分配:

在方法區中有類的常量池,常量池中保存着類的很多信息的符號引用,很多符號引用還轉換為了直接引用以使在運行的過程能夠訪問到這些信息的真實地址。

那么創建出的對象是怎么在堆中分配空間的呢?

首先我們要明確對象中存儲的大部分的數據就是它對應的非靜態字段和每個字段方法對應的方法區中的地址,因為這些東西每個對象都是不一樣的,所以必須通過各自的堆空間存儲這些不一樣的數據,而方法是所有同類對象共用的,因為方法的命令是一樣的,每個對象只是在各自的線程棧幀里提供各自的局部變量表和操作數棧就好。

這樣看來,堆中存放的是真正“有個性”的屬於對象自己的變量,這些變量往往是最占空間的,而這些變量對應的類字段的地址會找到位於方法區中,同樣的同類對象如果要執行一個方法只需要在自己的棧幀里面創建局部變量表和操作數棧,然后根據方法對應的方法區中的地址去尋找到方法體執行其中的命令即可,這樣一來堆里面只存放有限的真正有意義的數據和地址,方法區里存放共用的字段和方法體,能最大程度地減小內存開銷。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM