了解了class文件,我覺得就很有必要去了解一下JVM中的字節碼指令,那樣堆class文件以及JVM運行機制也後很大的幫助.
Java虛擬機的指令由一個字節長度的,代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其後的零至多個代表所需參數(稱為操作數,Oprands)而構成.由於Java虛擬機采用面向操作數棧而不是寄存器的架構,所以大多參數的指令都不包含操作數,只有一個操作碼.
由於限制了Java虛擬機操作碼的長度為一個字節(0~255),這意味著指令集的操作碼總數不可能超過256條.
由於class文件格式放棄了編譯後的操作數長度對齊,這意味著虛擬機處理那些超過一個字節數據的時候,不得不在運行時從字節中重建出具體數據的結構.如果要將一個16位長度的無符號整數,使用兩個無符號字節存儲起來(將它們命名為byte1和byte2),那它們的值應該是這樣的:(byte<<8)| byte2 .這種操作在某種程度上會導致解釋執行字節碼的時候會損失一些性能.但這樣做的優勢也是非常明顯的,放棄了操作數長度對齊,就意味著可以省略很多填充和間隔符號:用一個字節來表示操作碼,也是為了盡可能獲得短小精干的編譯代碼.
對於大部分與數據類型相關的字節碼指令它們的操作碼助記符中都有特殊特的字符來表明專門為哪種數據類型服務:i:代表int,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference.
Java虛擬機的指令集對於特定的操作只提供了有限的類型相關指令去支持它,換句話說,指令集將會故意被設計成非完全獨立的Java虛擬機規范中把這種特性稱為"Not Orthpogoal".即並非每種操作都有對應的指令.有一些單獨的指令可以在必要的時候來將一些不支持的類型轉換成為可被支持的類型.
加載和存儲指令用於將數據在棧幀中的局部變量和操作數之間來回傳輸.
將一個局部變量加載到操作棧:ilaod , ioload<n>,llaod , llaod<n>,float,float<n>,double,double<n>,aload,aload<n>.
將一個數據從操作數棧存儲到局部變量表:istore,istore<n>,lstore,lstore<n>,fstore.fstore<n>,dstore,dstore<n>,astore,asotre<n>.
將一個常量加載到數據棧:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
擴充局部變量表訪問索引的指令:wide
運算或算術指令用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂.
大體上算術指令可以分為兩種:對整型數據進行運算的指令對浮點數據進行運算的指令.無論是哪種算術指令,都使用Java虛擬機的數據類型,由於沒有直接支持byte,short,char和boolean類型的算術指令,對於這類數據的運算應使用操作int類型的指令代替.
加法指令:iadd,ladd,fadd,dadd
減法指令:isub,lsub,fsub,dsub
乘法指令:imul,lmul,fmul,dmul
求余指令:irem,lrem,frem,drem
求反指令:ineg,lneg,fneg,dneg
移位指令:ishl.ishr,lshl,lshr,lushr
按位或指令:ior,lor
按位與指令:iand,land]
按位異或指令:ixor,lxor
局部變量自增指令:iinc
比較指令:dcmpy,dcmpl,fcmpg,fsmpyl,lcomp
Java虛擬機要求在進行浮點數運算時,所有的運算結果都必須捨入到適當的精度,非精確的結果必須捨入為可被表示的最接近的精確值.如果有兩種可表示的形式與該值一樣接近,將優先選擇最低有效位為零的,稱為向最近數捨入模式.
在把浮點數轉換為整數時,Java虛擬機使用IEEE754規范中的向零捨入模式,這中模式的捨入結果會導致數字被截斷,所有小數部分的有效字節都會被丟棄掉.向零捨入模式將在目標數值類型與該數值類型中選擇一個最接近但是不大於原數值的數字來作為最精確的捨入結果.
Java虛擬機在處理浮點數運算時,不會拋出任何運行時異常,當一個操作產生溢出時,將會使用有符號的無窮大來表示,如果某個操作結果沒有明確的數學定義的話,將會使用NaN值來表示,所有使用NaN值作為操作數的算術運算,結果都會返回NaN.
在對long類型數值進行比較時,虛擬機采用帶符號的比較方式,而對浮點數值,進行比較時(dcmpy,dcmpl,fcmpg,fcmpl),虛擬機會采用IEEE754規范所定義的無信號比較(Nonsignaling Conparions)方式.
類型轉換指令可以將兩種不同的數值類型進行相互轉換
Java直接支持(即轉換時無需顯示進行轉換指令)以下數值類型的寬化類型轉化(即范圍類型向大范圍類型的安全轉化):
處理窄化類型轉換時,必須顯示的使用轉換指令來完成,這些轉換指令包括:i2b , i2c , i2s , l2i , f2c , d2i , d2l , d2f.窄化類型轉換可能會導致轉換結果產生不同的正負號,不同的數量級的情況,轉換可能會導致數值精度丟失.
將一個浮點型數值窄化為整型類型(int 或long)的時候,將遵循一下轉換規則
指令:
Java虛擬機提供了一些直接操作操作數的指令:
控制轉移指令可以讓Java虛擬機有條件的從指定的位置而不是控制轉移指令的下一條指令繼續執行程序。可以認為控制轉移指令就是在有條件或無條件的修改PC寄存器的值。
控制轉移指令:
invokevirtual指令用於調用對象德邦實例方法,根據對象的實體的類型進行分派(虛方法分派),也就是Java語言中最常見的方法分派方式。
invokeinterface指令用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象找出適合的方法進行調用。
invokespecial指令用於調用一些需要特殊處理的實例方法,包括實例初始化方法,私有方法和父類方法
involvestatic指令用於調用類的方法(static方法)
invokedynamic指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法
前面4條調用指令的分派邏輯都固化在Java虛擬機的內部,而invokedynamic指令的分派邏輯是由用戶設定的引導方法決定的
在Java虛擬機中處理異常catch語句不是由字節碼指令來實現的,而是用異常表來完成的。
在Java程序中顯式拋出異常的操作(throw語句)都是由athrow來實現的,除了用throw語句顯式拋出異常的情況之外,Java虛擬機規范還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。
Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步。這兩種同步結構都是使用管理(Monitor)來支持的。
方法級的同步是隱含的,既無需通過字節碼指令來控制,也實現在方法調用和返回操作之中。
同步一段指令序列通常是由Java語言中的synchronize語句塊來表示的,Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronize關鍵字的語義,正確實現synchronized關鍵字需要Java編譯器與Java虛擬機兩者共同協作支持