上一篇看得懂的字節碼講了怎麼看字節碼,以及字節碼中的魔數、版本號和常量池部分,這篇文章接着字節碼順序往下將。請必定要看了上一篇以後再看這一篇,由於有絕對的邏輯關係,不看上一篇這篇基本看不懂。java
若是這篇文章中有我沒有講到怎麼出現的東西,(好比程序的代碼,javap工具,查看字節碼的工具等)就是我在上一篇文章中已經提到過的,能夠查看個人上一篇文章來找到答案。git
上一篇地址 能看懂的字節碼-上github
個人全部文章同步更新與Github--Java-Notes,想了解JVM,HashMap源碼分析,spring相關能夠點個star,劍指offer題解(Java版),設計模式。能夠看個人github主頁,天天都在更新喲。spring
邀請您跟我一同完成 repo設計模式
緊接着常量池後面的兩個字節(u2類型)就表示訪問的標誌,在字節碼中是這兩個數數組
那麼他表明什麼呢?他表示這個類或者接口聲明爲 publicide
咱們看他的全部含義的表 函數
21就是 ACC_PUBLIC 和 ACC_SUPER 這兩個標誌值相或的結果。即 0x0001 | 0x0020 = 0x0021。工具
而咱們的代碼中爲源碼分析
public class TestClass {
.......
}
複製代碼
你能夠試下將它聲明爲 private 或者其餘狀況是什麼值
這三個很是相似,因此放在一塊兒講。類索引和父類索引都是u2的數據。接口索引是一組u2類型的數據。由於Java只能單繼承,可是能夠實現多個接口
咱們回到字節碼文件,接着上次看到的 0x0021.
後面是 0x0003, 0x0004,0x0000。這些啥意思呢?
剛剛說了指向CONTANT_Class_info,而這個class_info 存了全限定名的信息,結構以下
在上一篇我也說了,由於他已經肯定了類型,因此是沒有tag的位置的,就只有後面的index。因此0x0003表示它指向 第三個常量。第三個常量是什麼呢?
咱們回到命令行的內容(這個在上篇介紹過怎麼打開的),
第三個常量指向第17個常量,第17個常量是一個 uft8類型的。他的信息爲·com/swu/leosanqing/TestClass
。這個不就是該類的名稱嗎?
同理,咱們找到 0x0004指向的含義,他是指向第18個常量,第18個常量也是一個utf8類型的數據,所存儲的信息爲 java/lang/Object
,這個類不就是咱們TestClass的父類嗎/
0x0000 表示這個類沒有實現接口(或者這個接口沒有繼承其餘接口),因此這三個就已經解析完了。
字段表(field_info)用於描述接口或類中聲明的變量,字段包括類級變量以及實例級變量,但不包括在方法內部聲明的局部變量
而這個field_info是一個這樣的結構
他跟以前的類的訪問標誌很像,都是u2類型的數據,可是有一些差異
若是是相同的話,那麼他們的標誌值是如出一轍的
緊隨access_flags後面的兩個是 name_index和descriptor_index,這兩個分別表示字段的簡單名稱以及字段和方法的描述符。
簡單名稱是指沒有類型和參數修飾的方法或者字段的名字,好比咱們定義的方法
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
複製代碼
那麼簡單名稱就是 "inc"和"m".等會兒你跟着我翻譯一下字節碼就知道了
字段和方法的描述符稍微複雜一些,他的做用是用來描述字段的數據類型、方法的參數列表(包括數量、類型和順序,看到這三個有沒有想到啥?沒錯,重載!等下就知道重載爲啥要三個不同)和返回值。
根據描述符規則,基本數據類型(byte,short,int…….)以及表明無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L+對象的全限定名來表示。
對於數組來講,每個字符將使用一個前置的"["字符來描述,
下面我來分別舉個例子
/* 若是我要表示一個整形數組 int[] 那麼就應該表示爲 [I 若是我要表示一個二維的數組char[][] 那麼就應該表示爲 [[C 若是我要表示一個二維的字符串數組String[][] 那麼就應該表示爲 [[Ljava/lang/String; 注意有分號 由於String不是基本類型也不是void類型,因此要用對象類型來描述 因此他就應該用字符L+String類的全限定名來表示 若是我要表示一個自定義的Person類,他在包 com.swu.leosanqing下 那麼就應該表示爲 Lcom/swu/leosanqing/Person */
複製代碼
若是描述符(descriptor_index)要描述一個方法的話,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號"()"中。
下面我舉幾個例子
/* 先拿本程序舉例,他有一個 方法 public int inc(){ return m+1; } 那麼就應該表示爲 "()I" 假設 Person類 在包 "com.swu.leosanqing"下 若是一個方法爲 public Person (char[] source,int[] a,char[][] c,int b,int c,String d). 那麼他就應該表示爲 ([C[I[[CIILjava/lang/String;)Lcom/swu/leosanqing; */
複製代碼
這個後面的方法表集合中會用到
咱們接着類索引、父類索引和接口索引往下看。
咱們對着下面的表進行一一對應,他前面還有一個 field_count
0x0001對應field_count
表示只有一個參數,由於代碼中咱們就只定義了一個 private int m
0x0002對應access_flags
,而後咱們查上面的表,它的意思爲 字段是private
而後咱們再看 0x0005,它對應的是u2類型的 name_index
,咱們應該找常量池中的第5個參數,咱們經過命令行中的找,他的名字是 "m"
而後咱們再往下走,0x0006 ,它對應descriptor_index
,而後咱們去常量池中尋找第6個常量,他是"I",而後後面兩個字節是0x0000,表示沒有屬性值。若是個人程序是 private int m = 123;
那麼後面的屬性值就應該有表示的值
因此看下來,就是 private int m,跟咱們程序中定義的是如出一轍的,代表咱們的解讀正確。
Java中沒有用過字段的重載吧?也就是說咱們不能在程序中這樣定義。
private int m;
public String m;
// 咱們都知道這樣作ide會報錯,兩個字段的數據類型、修飾符不論是否相同
// 都必須使用不同的名字
//可是在字節碼中就不同,若是兩個字段的描述符不一致,那麼他是能夠重名的,且是合法的
複製代碼
下一個小節將方法的重載,這個應該就稍微清晰一點
若是你理解了上一小節的內容,那麼這一小節將會比較好懂,由於兩個都差很少。
他就是用來表示字節碼中的方法的各類數據項
你看這個表是否是很眼熟,和上一小節的字段表結構很是像?
訪問標誌也很像不過有些增減,好比加了方法的修飾符 native 的標誌,刪去了字段的 volatile 標誌
有沒有發現,方法表結構並無表示或者存放代碼的內容,那麼他去哪裏了呢?他存放到方法表中的屬性表裏的 "Code"屬性裏。我會在下一節中講到。
咱們先看下咱們的方法對應字節碼中的是哪些東西。這個恰好是接着上一節的字段表的位置開始的
跟上一節的字段表同樣,首先他有一個表示方法多少的計數器"methods_count",而後纔是方法表(method_info)裏面的數據,因此 0x0002表示裏面有兩個方法。
你可能疑惑了,我只定義了一個inc()方法啊,怎麼會有兩個呢?別忘了,咱們還有一個默認的構造函數。因此有兩個
而後咱們再對應方法表來看
咱們再看下一個 0x0001,它對應access_flags
,咱們查方法訪問標誌表,發現他是一個public
而後咱們再看下一個0x0007,他對應name_index
,咱們去常量池找第七個常量,發現他是 <init>
,這個表示實例構造器.
而後看下一個 0x0008,它對應描述符的索引(descriptor_index),去常量池找,發現他是()V,、
再看下一個 0x0001,它對應屬性值的的個數(attributes_count),說明有1個。
再看下一個 0x0009,他對應屬性值(attributes),咱們查看常量池發現他是表示"Code",Code也是一個複合表,他的各項定義在下一節中會講到。
因此綜合起來這些字節碼就表示 有兩個方法,其中一個是構造函數public void
那咱們定義的public int inc()
方法哪去了?這兩個離的比較遠,由於裏面還有其餘的東西。這些東西是啥,下節講屬性表會介紹
根據剛剛分析第一個方法的方法,咱們能夠得出,解析出來是public int inc()
,徹底是咱們定義的方法。
Java中,方法的重載只要知足是三個條件的其中一個就行:參數個數不一樣,參數類型不一樣,參數順序不一樣。
那是由於Java代碼中要重載一個方法,除了要有相同的名字以外,還得有不一樣的特徵簽名。這個特徵簽名只包括了方法名稱、參數順序、參數類型。
可是字節碼中,特徵簽名不只包括上面的部分,還要包括返回值和受查異常表,若是僅僅只是Java代碼部分的特徵簽名相同,可是返回值或者受查異常表不一樣,那麼字節碼也是容許那樣的同名方法。
屬性表(attribute_info)在以前的小節中出現了屢次,它是用來描述某些場景的專有信息
好比咱們以前說的Code,還有在字段表小節說的假如 咱們有一個代碼是private int m =123;
,屬性表就存123
的值。
虛擬機規範預約義了很是很是多屬性
對於每個屬性,他的名稱須要從常量池中引用一個CONSTANT_Utf8_info
類型的常量來表示,而這個屬性的結構徹底是自定義的,只須要經過一個u4的長度屬性去說明屬性值所佔用的位數便可。
咱們來看Code屬性表結構(不是每個方法表都有Code屬性,好比接口和抽象類中的方法就沒有這個)
因爲這是Code表結構,並且以前的方法表已經記錄了Code前兩個數據項的內容,因此屬性值的長度固定爲整個屬性表長度減去6個字節,即減去 (u2+u4)
咱們先看他的範圍
前面的4個字節(0x0000001D)對應attribute_length
,表示這個Code屬性表一共有多長,總共佔用多少個字節。咱們換算成十進制,就是29,咱們數一下他後面的,恰好是29個字節。從 03後面就是第二個方法的存儲位置了,恰好對應上一節的第二個方法的起始位置,能夠翻回去驗證一下(不過以前的紅線少畫了兩個字節,分析的時候加上了)
後面的 0x0001,0x0001,0x00000005分別對應 max_stack
,max_local
,code_length
.
後面的 2A B7 00 01 B1
分別就是字節碼指令了。咱們來翻譯一下
invokespecial
,這條指令的做用是以棧頂的reference 類型的數據所指向的對象做爲方法接受者,調用此對象的實例構造器方法、private方法或者他的父類的方法。這個方法有一個u2類型的參數說明具體調用哪個方法,他指向常量池中的一個CONSTANT_Methodref_info 類型常量,即次方法的方法符號調用(就是下一個參數)invokespecial
的參數,查常量池得 0x0001對應的常量爲 實例構造器"<init>"
方法的符號引用咱們經過上述的字節碼指令能夠看出,虛擬機的這個執行過程是基於棧的,因此咱們能夠初步推測,Java虛擬機執行字節碼是基於棧的體系結構
後面的那些字節碼是什麼呢?
00 00 00 01 00 0A 咱們接着看code表後面的內容
他的意思分別是 exception_table_length參數爲0,attribute_count參數爲1,0A表示指向常量池的第10個常量,也就是LineNumberTable。
咱們來看LineNumberTable的結構
他跟屬性表結構CONTANT_Utf8_info差不太多,因此咱們能夠省掉第一個,由於已經知道他是這個名字了,而後再接着上面的字節碼,解析一下他們分別表示啥。
首先他有一個u4類型的數據表示長度,因此 0x00000006就表示後面的6個字節用來描述這個屬性。後面恰好只有6個字節。
而後一個 u2類型的 line_number_table_length這個就表示有一個行號表,行號表只有兩個u2類型的數據,start_pc和line_number,前者表示字節碼行號,後者表示Java源碼行號,因此分別是 0和3
經過命令行的結果來驗證咱們是否解析的正確
咱們看到和咱們的解析結果一致。因此咱們的解析過程是正確的
後面的第二個方法你能夠按照個人思路本身解析一下,應該沒有太大的問題。
至此字節碼基本已經解析完
經過兩篇文章咱們知道了字節碼文件中有哪些東西,又該怎麼去看字節碼文件。
Class文件中一共就只有兩種數據類型
Class文件中一共有哪些東西呢?大的分類一共有下面幾種
固然我這個程序只是很是簡單的程序,還有不少內容用不到,好比異常、接口、同步鎖等,可是已經能夠將整個Class串起來了,若是你想驗證,能夠根據個人方法,自行驗證