前面咱們講解了Class文件的結構、以及採用不一樣的方式來解讀Class文件html
可是針對於Class文件裏方法的字節碼指令,咱們並無進行細節的指令分析java
本篇文章,咱們開始對字節碼指令進行分析,看看示例代碼裏的方法到底作了什麼事情?數組
Java字節碼對於虛擬機,就好像彙編語言對於計算機,屬於基本執行指令。緩存
Java虛擬機的指令由一個字節長度
的、表明着某種特定操做含義的數字
(稱爲操做碼,Opcode)以及跟隨其後的零至多個表明此操做所需參數
(稱爲操做數,Operands)而構成。oracle
因爲Java虛擬機採用面向操做數棧而不是寄存器的結構,因此大多數的指令都不包含操做數jvm
咱們能夠採用上一篇文章的示例代碼與字節碼分析進行解析看看ide
咱們根據上篇的思路,找找這些字節碼指令對應的字節碼是什麼呢?表明什麼意思呢?性能
虛擬機限制了Java 操做碼的長度爲一個字節(即0~255),這意味着操做碼總數不可能超過256條學習
官方文檔: https: //docs.oracle.com/javase/specs/jvms/se8/htm1/jvms-6.html
ui
熟悉虛擬機的指令對於動態字節碼生成、反編譯Class文件、Class文件修補都有着很是重要的價值
。所以閱讀學節碼做爲了解ava虛擬機的基礎技能,須要熟練掌握常見指令。
================================
若是不考慮異常處理的話
那麼Java虛擬機的解釋器可使用下面這個僞代碼當作最基本的執行模型來理解
do{ 自動計算PC寄存器的值加1; 根據PC寄存器的指示位置,從字節碼流中取出操做碼; if(字節碼存在操做數)從字節碼流中取出操做數; 執行操做碼所定義的操做; }while(字節碼長度 > 0);
================================
在Java虛擬機的指令集中,大多數的指令都包含了其操做所對應的數據類型信息。例如:
咱們能夠根據上篇的示例代碼進行解析分析看看
咱們能夠看看局部變量表裏的索引:0的值是什麼?
對於大部分與數據類型相關的字節碼指令,它們的操做碼助記符中都有特殊的字符來代表專門爲哪一種數據類型服務
:
也有一些指令的助記符中沒有明確地指明操做類型
的字母,如arraylength指令沒有表明數據類型的特殊字符,但操做數永遠只能是一個數組類型的對象
。
還有另一些指令,如無條件跳轉指令goto
則是與數據類型無關
的。
可是大部分的指令都沒有支持整數類型byte、char和short,甚至沒有任何指令支持boolean類型
編譯器會在編譯期或運行期將byte和short類型的數據帶符號擴展(Sign-Extend)爲相應的int類型數據
,將boolean和char類型數據零位擴展(Zero-Extend)爲相應的int類型數據
。
與之相似,在處理boolean、byte、short和char類型的數組時,也會轉換爲使用對應的int類型的字節碼指令來處理。所以大多數對於boolean、byte、short和char類型數據的操做,實際上都是使用相應的int類型做爲運算類型
。
byte b1 = 12; short s1 = 10 int i = b1 + s1
================================
因爲徹底介紹和學習這些指令須要花費大量時間。爲了讓你們可以更快地熟悉和了解這些基本指令,這裏將JVN中的字節碼指令集按用途大體分紅9類
寫在前面的,關於這些不一樣分類指令,大多在作值相關操做時:
一個指令能夠從局部變量表、常量池、堆中對象、方法調用、系統調用中等取得數據,這些數據(多是值,多是對象的引用)被壓入操做數棧。
一個指令也能夠從操做數棧中取出一到多個值(pop屢次),完成賦值、加減乘除、方法傳參、系統調用等等操做。
================================
加載和存儲指令用於將數據從棧幀的局部變量表和操做數棧之間來回傳遞
。
================================
上面所列舉的指令助記符中,有一部分是以尖括號結尾的(例如:iload_<n>
)。
指令助記符實際上表明瞭一組指令
例如:iload_<n>
表明了iload_0、iload_一、iload_二、iload_3
這幾個指令。
這幾組指令都是某個帶有一個操做數的通用指令(例如:iload
)的特殊形式,對於這若干組特殊指令來講,它們表面上沒有操做數,不須要進行取操做數的動做,但操做數都隱含在指令中
。
除此以外它們的語義與原生的通用指令徹底一致
例如 iload_0
的語義與操做數爲時的 iload 指令語義徹底一致。
示例舉例:
iload_0:將局部變量表中索引爲0位置上的數據壓入操做數棧中,這是佔一個字節
iload 0:將局部變量表中索引爲0位置上的數據壓入操做數棧中,這是佔兩個字節
在尖括號之間的字母指定了指令隱含操做數的數據類型,具體信息以下:
<n>表明非負的整數、<i>表明是int類型
<l>表明long類型、<f>表明float類型
<d>表明double類型
操做byte、char、short和boolean類型數據時,常常用int類型的指令來表示。
================================
咱們知道Java字節碼是Java虛擬機所使用的指令集。所以與Java虛擬機基於棧的計算模型是密不可分
在解釋執行過程當中每當爲Java方法分配棧楨時,Java虛擬機每每須要開闢一塊額外的空間做爲操做數棧,來存放計算的操做數以及返回結果
。
具體來講即是:執行每一條指令以前,Java虛擬機要求該指令的操做數已被壓入操做數棧中。在執行指令時,Java虛擬機會將該指令所需的操做數彈出,而且將指令的結果從新壓入棧中
。
以加法指令 iadd爲例。假設在執行該指令前,棧頂的兩個元素分別爲 int值 1和 int 值 2,那麼iadd 指令將彈出這兩個int,並將求得的和int值 3 壓入棧中。
因爲 iadd
指令只消耗棧頂的兩個元素,所以,對於離棧頂距離爲?的元素,即圖中的問號,iadd
指令並不關心它是否存在,更加不會對其進行修改。
================================
Java方法棧楨的另一個重要組成部分則是局部變量區,字節碼程序能夠將計算的結果緩存在局部變量區之中
。
實際上,Java虛擬機將局部變量區當成一個數組,依次存放 this 指針〈僅非靜態方法),所傳入的參數,以及字節碼中的局部變量。
和操做數棧同樣,long類型
以及 double類型
的值將佔據兩個單元,其他類型僅佔據一個單元。
舉例: public vid foo( long l,fl1oatf) { { int i = 0; } { string s = "He11o, wor1d" ; } }
在棧幀中,與性能調優關係最爲密切的部分就是局部變量表
。局部變量表中的變量也是重要的垃圾回收根節點(GC Roots),只要被局部變量表中直接或間接引用的對象都不會被回收
。
在方法執行時,虛擬機使用局部變量表完成方法的傳遞
局部變量壓棧指令將給定的局部變量表中的數據壓入操做數棧。
這類指令大致能夠分爲:
xload_<n>
,描述爲:x爲i、l、f、d、a,n爲0到3
xload
,描述爲: x爲i、l、f、d、a
說明:在這裏x
的取值表示數據類型
指令xload_n
表示將第n個局部變量壓入操做數棧
好比iload_一、fload_0、aload_e
等指令。其中aload_n
表示將個對象引用壓棧
。
指令xload
經過指定參數的形式,把局部變量壓入操做數棧,當使用這個命令時,表示局部變量的數量可能超過了4個,好比指令iload、 fload等。
接下來使用示例代碼來演示一下局部變量壓棧指令
public class LoadAndStoreTest { //1.局部變量壓棧指令 public void load(int num,object obj,long count,boolean flag,short[] arr){ system.out.println(num); system.out.println(obj); system.out.println(count); system.out.print1n(flag); system.out.println(arr); } }
咱們使用編譯一下,而且在idea中使用插件來看看該方法具體的指令有哪些?
此時咱們根據這些指令進行分析看看,而且看看局部變量表與操做數棧是怎麼樣的狀況
咱們也可使用idea的插件校驗一下,看看是否方法裏的局部變量表一致?
接下來咱們分析一下指令是怎麼操做局部變量表與操做數棧的,
當咱們操做局部變量表索引爲:5的時候,就會發現它佔用了兩個字節:iload 5,why?
常量入棧指令的功能是
常數壓入操做數棧,根據數據類型和入棧內容的不一樣,又能夠分爲const系列、push系列和ldc指令。
================================
用於對特定的常量入棧,入棧的常量隱含在指令自己裏。
指令有: iconst_<i>
、描述:i從-1到5
指令有: lconst_<l>
、描述:l從0到1
指令有: fconst_<f>
、描述:f從0到2
指令有: dconst_<d>
、描述:d從0到1
指令有: aconst_null
、描述:d從0到1
好比有示例:
從指令的命名上不難找出規律,指令助記符的第一個字符老是喜歡錶示數據類型。
若是指令隱含操做的參數,會如下劃線形式給出。
================================
主要包括bipush和sipush
,它們區別在於接收數據類型的不一樣
:
bipush接收8位整數做爲參數、sipush接收16位整數,它們都將參數壓入棧。
================================
若是以上指令都不能知足需求,那麼可使用萬能的ldc指令,它能夠接收一個8位的參數,該參數指向常量池中的int、float或者String的索引,將指定的內容壓入堆棧
。
相似的還有ldc_w,它接收兩個8位參數,能支持的索引範圍大於ldc。
若是要壓入的元素是1ong或者double類型的,則使用1dc2_w指令
,使用方式都是相似的。
接下來使用示例代碼來演示一下常量壓棧指令
public class LoadAndStoreTest { //2.常量入棧指令 public void pushConstLdc() { int i = 1; int a = 5; int b = 6; int c = 127; int d = 128; int e = 32767; int f = 32768; } }
咱們使用編譯一下,而且在idea中使用插件來看看該方法具體的指令有哪些?
雖然咱們都是int類型的變量,可是指令裏也有byte、long、short這些類型
因此咱們能夠總結一下,具體類型的範圍具體定義,能夠看以下圖:
那麼對於float、long類型,咱們也進行示例代碼看看具體是怎麼樣的?
public class LoadAndStoreTest { //2.常量入棧指令 public void constLdc() { 1ong a1 = 1; long a2 = 2; float b1 = 2; f1oat b2 = 3; double c1 = 1; double c2 = 2; Date d = null; } }
咱們使用編譯一下,而且在idea中使用插件來看看該方法具體的指令有哪些?
咱們前面也提到過壓入的元素是1ong或者double類型的,則使用ldc2_w指令
當咱們超出float類型的範圍一樣也是使用ldc2_w的指令
出棧裝入局部變量表指令
用於將操做數棧中棧頂元素彈出後,裝入局部變量表的指定位置,用於給局部變量賦值
這類指令主要以store的形式存在
指令:xstore
,描述:x爲i、l、f、d、a
、
指令:xstore_n
,描述:x 爲i、l、f、d、a,n爲至 3
。
其中指令istore_n
將從操做數棧中彈出一個整數,並把它賦值給局部變量索引n位置
指令xstore因爲沒有隱含參數信息,故須要提供一個byte類型的參數類指定目標局部變量表的位置
接下來使用示例代碼來演示一下常量壓棧指令
public class LoadAndStoreTest { //3.出棧裝入局部變量表指令 public void store(int k,double d){ int m = k + 2; long l = 12; string str = "atguigu"; float f = 10.0F; d = 10; } }
咱們使用編譯一下,而且在idea中使用插件來看看該方法具體的指令有哪些?
此時咱們根據這些指令進行分析看看,而且看看出棧指令是怎麼樣的狀況
接下來使用示例代碼來演示一下其餘的狀況說明
public class LoadAndStoreTest { //4.出棧裝入局部變量表指令 public void foo( 1ong l,f1oat f){ { int i = 0; } { string s = "He1lo,wor1d" } } }
咱們使用編譯一下,而且在idea中使用插件來看看該方法具體的指令有哪些?
僅接着咱們來看看局部變量表裏有什麼呢?
可是咱們的局部變量表長度是多少呢?咱們一塊兒來看看
此時咱們根據這些指令進行分析看看,而且看看出棧指令是怎麼樣的狀況