摘要:對一個文件的字節碼進行逐行的分析是理解文件結構的最佳方式。本文的目的在于盡可能完整地拆解的字節碼并將其分塊分析,最終得到的圖解結構希望可以幫助到你。字節碼指令的具體含義鑒于與結構是相對獨立的主題不再詳述,后續會再多帶帶深入介紹。
對一個class文件的字節碼進行逐行的分析是理解class文件結構的最佳方式。但是往往復雜的二進制字節碼會讓人望而卻步,或者只有仔細一點點盯著才能保證不花眼。本文的目的在于盡可能完整地拆解JVM的Class字節碼并將其分塊分析,最終得到的圖解結構希望可以幫助到你。
本文參考自來自周志明《深入理解Java虛擬機(第2版)》,拓展內容建議讀者可以閱讀下這本書。根據這個簡單的例子來說明
以下的例子作為最簡單的一個java程序,通過javac執行編譯,javap來查看它的反編譯結果,當然我們還會更刨根問底地直接使用二進制編輯器查看class文件的二進制字節排布。
> javap -v Test Classfile /Users/jinhaoplus/Desktop/Test.class Last modified 2018-8-12; size 285 bytes MD5 checksum eac8f02f8ad176b09bfd89cf15e2ed3d Compiled from "Test.java" public class top.jinhaoplus.demo.Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#15 // java/lang/Object."圖解概況":()V #2 = Fieldref #3.#16 // top/jinhaoplus/demo/Test.m:I #3 = Class #17 // top/jinhaoplus/demo/Test #4 = Class #18 // java/lang/Object #5 = Utf8 m #6 = Utf8 I #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 inc #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 Test.java #15 = NameAndType #7:#8 // " ":()V #16 = NameAndType #5:#6 // m:I #17 = Utf8 top/jinhaoplus/demo/Test #18 = Utf8 java/lang/Object { public int m; descriptor: I flags: ACC_PUBLIC public top.jinhaoplus.demo.Test(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 3: 0 public int inc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 6: 0 } SourceFile: "Test.java"
如上的字節碼閱讀起來有諸多障礙,因此我們把上面的字節碼按照字節碼規范定義的class結構分區為不同的顏色塊,不同的分區顏色說明這個區域對應著class結構中的不同區域定義,表示一個整體概念的字節碼在圖中顯示為同一行上:
下面的圖是class文件結構的思維導圖說明,可以跟上述的實際的一個class的分區作簡單的對照:
詳細解釋一下class文件的每個分區下面詳細解釋一下class文件的每個分區,括號內的數字表示當前區的占位情況,u是字節的意思,如u4表示占4個字節的空間,對應到圖中就是4個方格。
1. magicmagic(u4):魔數,class文件的標識開頭。
CAFEBABE是固定的JVM Class的魔數,也可以認為是眾所周知的Java咖啡Logo的由來。
2. versionversion:class版本,主次版本合起來即可確定版本號。
2.1 minor_version(u2):次版本
2.2 major_version(u2):主版本
Class文件的版本為次版本0X0000、主版本0X0034,對應的是10進制的52.0。說明此Class是在JDK_VERSION=52.0(JDK1.8)的編譯器中生成的,同時又可以被版本在JDK_VERSION=52.0及以上的虛擬機上執行(JVM保持了向下兼容性,但是拒絕執行超過它的版本號的Class字節碼)。
3. 常量池:注意是本處的常量池指class字節碼中的常量池而非JVM中的常量池(但后者中的數據其實是加載于前者)。 3.1 constant_pool_countconstant_pool_count(u2):常量池大小,定義了常量池中保存的常量個數(準確說常量個數=constant_pool_count-1)。
0X0013表示constant_pool_count=19,常量池中保存的常量個數=18(編號為#1~#18)。
3.2 constant_poolconstant_pool(constant_pool_count-1個constant_pool_info):實際保存的常量,編號從1開始(將0位留空有特殊考量)。
常量有多種種類,我們這里只提一下我們的Class文件里涉及到的具體的類型。
由utf-8編碼的二進制串,其字節碼格式為
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
其中的tag=0X01即為CONSTANT_Utf8_info類型常量的標識。我們Class字節碼中的#5、#6、#7、#8、#9、#10、#11、#12、#13、#14、#17、#18都是CONSTANT_Utf8_info常量,因為它們的首位tag=0X01(橘色列),通過utf-8解碼這些常量指定長度的二進制串可以得出下面的結果,比如#5號常量length=1(10進制的0X0001),而bytes為0X6D,utf-8解碼后就是字符串m,同理可以得到這些二進制串的值(這就是javap反編譯出結果的原理,可以參照javap得到的結果對照一下):
#5 m #6 I #7#8 ()V #9 Code #10 LineNumberTable #11 inc #12 ()I #13 SourceFile #14 Test.java #17 top/jinhaoplus/demo/Test #18 java/lang/Object
類常量,其字節碼格式為
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
其中的tag=0X07即為CONSTANT_Class_info類型常量的標識,index指向了常量池中類的全限定名的索引序號。
我們Class字節碼中的#3、#4是CONSTANT_Class_info類型的類常量,它們的首位tag=0X07(橘色列),通過查找常量池中它們指向的索引序號,我們可以得出這兩個類的全限定名:
#3 #17 // top/jinhaoplus/demo/Test #4 #18 // java/lang/Object
字段或方法的名稱和類型常量,其字節碼格式為
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
u2 | index | 1 |
其中的tag=0X0C即為CONSTANT_NameAndType_info類型常量的標識,第一個index指向了字段或方法名稱在常量池中的索引序號,第二個index指向了字段或方法的描述符在常量池中的索引序號。
字段的描述符就是簡單的字段類型,Class文件中的類型為了節省空間進行了簡化:如基本類型int->I,double->D,引用類型java/lang/Object -> Ljava/lang/Object。
我們Class字節碼中的#15、#16是CONSTANT_NameAndType_info類型的類常量,它們的首位tag=0X0C(橘色列),通過查找常量池中它們兩個指向的索引序號,我們可以得出常量#15的名稱為#7號常量即
#15 #7:#8 // "":()V #16 #5:#6 // m:I
字段引用常量,其字節碼格式為
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
u2 | index | 1 |
其中的tag=0X09即為CONSTANT_Fieldref_info類型常量的標識,第一個index指向了聲明字段的類或接口的CONSTANT_Class_info常量在常量池中的索引序號,第二個index指向了字段的名稱和類型信息CONSTANT_NameAndType_info常量在常量池中的索引序號。
我們Class字節碼中的#2是CONSTANT_Fieldref_info類型的類常量,它們的首位tag=0X09(橘色列),通過查找常量池中它指向的索引序號,我們可以得出這個字段的聲明類的是top/jinhaoplus/demo/Test,字段的名稱是m,類型是I(即int,Class將類型全稱映射到成了單字母)。
#2 #3.#16 // top/jinhaoplus/demo/Test.m:I
方法引用常量,其字節碼格式為
類型 | 名稱 | 數量 |
---|---|---|
u1 | tag | 1 |
u2 | index | 1 |
u2 | index | 1 |
其中的tag=0X0A即為CONSTANT_Methodref_info類型常量的標識,第一個index指向了聲明方法的類或接口的CONSTANT_Class_info常量在常量池中的索引序號,第二個index指向了方法的名稱和類型信息CONSTANT_NameAndType_info常量在常量池中的索引序號。
我們Class字節碼中的#1是CONSTANT_Fieldref_info類型的類常量,它們的首位tag=0X0A(橘色列),通過查找常量池中它指向的索引序號,我們可以得出這個方法的聲明類是java/lang/Object,方法的名稱是
#1 #4.#15 // java/lang/Object."":()V
至此我們得到了這個Class中的常量池中全部的常量的含義。這些常量將被下面的其他部分引用到。
4.類信息: 4.1 access_flagaccess_flag(u2):說明這個類或接口的訪問標志,如private/public/interface/abstract/annotation/enum等,總之是說明了這個類的特征。以不同的特征給出特征位的方式來設置這個u2大小的區域。
如本Class的0X0021實際代表了特征位信息是0000000000110001,即ACC_SUPER|ACC_PUBLIC,表示它是public的class(ACC_SUPER是JDK1.0.2后的默認設置項)。
this_class(u2):說明本類的類索引,0X0003說明本類索引在常量池中的序號為3,上面常量池的分析可以看到本類的全限定名是top/jinhaoplus/demo/Test。
4.3 super_classsuper_class(u2):說明父類的類索引,0X0004說明父類索引在常量池中的序號為4,上面常量池的分析可以看到父類的全限定名是java/lang/Object(這也就是所有Java類的父類都是Object的原因,即使沒有明確寫出來編譯后的Class文件中也會將這個父類聲明定義出來)。
4.4 interface_info類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
比如private/public/protected/static/final/volatile,以不同的特征給出特征位的方式來設置這個u2大小的區域。我們的Class中的0X0001(橙色列)實際代表了特征位信息是0000000000000001,即字段的特征是ACC_PUBLIC(public字段)。
我們的Class中的0X0005(藍色列)指向的常量池中的#5號常量即m。
我們的Class中的0X0006(青色列)指向的常量池中的#6號常量即I。
5.2.4.1 attributes_count(u2):字段屬性表的屬性數量,我們的Class中的0X0000表示本字段無額外的屬性表信息。
5.2.4.1 attributes(attributes_count個attribute_info):字段屬性表的屬性信息,字段屬性有自己定義的結構,字段中主要使用的屬性包括ConstantValue(final修飾的常量值作為字段的值)、Depreciated(@Depreciated修飾的字段表示棄用)、Signature(泛型參數記錄的泛型簽名信息,否則編譯后擦除類型就無法溯源了)等,他們都有各自定義的結構。
6.方法信息 6.1 method_countmethod_count(u2):方法數量
我們的Class這個區的0X0002表示這個類有兩個方法。
method_info(method_count個method_info):方法信息,方法表的結構如下:
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
比如private/public/protected/static/final/synchronized,以不同的特征給出特征位的方式來設置這個u2大小的區域。我們的Class中兩個方法的這個區域的0X0001(橙色列)實際代表了特征位信息是0000000000000001,即它們的特征都是ACC_PUBLIC(public方法)。
我們的Class中,method_#1的0X0005(藍色列)指向的常量池中的#7號常量即
我們的Class中,method_#1的0X0008(青色列)指向的常量池中的#8號常量即()V,而。method_#2的0X000C(青色列)指向的常量池中的#12號常量即()I。
6.2.4.1 attributes_count(u2):方法屬性表的屬性數量。
6.2.4.2 attributes(attributes_count個attribute_info):方法屬性表的屬性信息,方法屬性有自己定義的結構,方法中主要使用的屬性包括最重要的Code(方法的字節碼指令,沒有方法執行體的接口和抽象類是沒有這個屬性的)、Exceptions(聲明方法拋出的異常)、Depreciated(@Depreciated修飾的方法表示棄用)、Signature(泛型參數記錄的泛型簽名信息)等,他們都有各自定義的結構。這里我們具體來看一下最重要的Code屬性。
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
a. attribute_name_index(u2):屬性名在常量池中的索引序號,Code屬性最終找到的常量肯定是Code。
b. attribute_length(u4):該屬性的長度。
c. max_stack(u2):該方法的操作數棧最大深度。
d. max_locals(u2):該方法的局部變量表的大小。
e. code_length(u4):字節碼指令的大小
f. code(exception_table_length個u1):字節碼。
g. exception_table_length(u2):異常表大小。
h. exception_table(exception_table_length個exception_info):異常表大小。
i. attributes_count(u2):方法屬性表的大小。
j. attributes(attribute_count個attribute_info):方法屬性表。
接下來用我們Class的兩個方法來詳細說明Code屬性:
method_#1方法:
i. attributes_count = 1
ii. attributes:
a. attribute_name_index:常量0X0009即Code。
b. attribute_length:29(0X0000001D),即下一位起后的29u都是這個屬性。
c. max_stack:1(0X0001)。
d. max_locals:1(0X0001)。
e. code_length:5(0X00000005)。
f. code:0X2AB70001B1。(字節碼指令的具體含義鑒于與class結構是相對獨立的主題不再詳述,后續會再多帶帶深入介紹)
g. exception_table_length:0(OX0000)。
h. exception_table:無。
i. attributes_count:1(0X0001)。
j. attributes:
attribute_name_index:LineNumberTable(0X000A),說明這是一個用于記錄源碼行號和字節碼行號映射的屬性表。
attribute_length:6(0X00000006).
attribute:LineNumberTable屬性表的內部結構:
line_number_table_length:1(0X0001)。
line_number_index:0:3(0X00000003)。
method_#2方法的分析方式如上類似不再贅述。
Class字節碼的結構為什么這么設計乍一看來上面的結構讓人很難快速理解,但是如果理解JVM的字節碼結構的設計目的就可以加深理解了。
JVM的字節碼結構其實是一種由字節碼堆砌的表型結構,充分定義占位的結構可以無歧義地將它想要表達的原義還原回去。作為二進制結構主要的表達方式,只要定義好占位情況,表型結構可以通過層層嵌套定義來實現更為復雜的結構、并且可以實現良好的拓展。
比如上面的介紹的方法信息通過方法數量定義了這個表的大小,而每個表entry內部可以再有自己的定義,比如方法信息中還可以包含屬性表(即在方法表內部再嵌套一層表),比如這里定義了Code屬性表,而Code屬性表自身又有良好的表結構定義,這個表內部除了一些一維的字段(比如index、count等不能拓展的字段)外,還有額外的exception_table,但是因為有exception_table_length的表大小限制就可以無歧義地還原回去,此外還有attribute_info,但是因為有attribute_count的表大小限制也可以無歧義地還原回去。用下面的思維導圖我們可以直觀地看出來這種良好的定義,圖中加入了每個一維節點的占位大小:
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76736.html
摘要:不同于個人面經,這份面經具有普適性。我在前面的文章中也提到了應該怎么做自我介紹與項目介紹,詳情可以查看這篇文章備戰春招秋招系列初出茅廬的程序員該如何準備面試。是建立連接時使用的握手信號。它表示確認發來的數據已經接受無誤。 showImg(https://segmentfault.com/img/remote/1460000016972448?w=921&h=532); 該文已加入開源文...
摘要:阻塞,非阻塞首先,阻塞這個詞來自操作系統的線程進程的狀態模型網絡爬蟲基本原理一后端掘金網絡爬蟲是捜索引擎抓取系統的重要組成部分。每門主要編程語言現未來已到后端掘金使用和在相同環境各加載多張小圖片,性能相差一倍。 2016 年度小結(服務器端方向)| 掘金技術征文 - 后端 - 掘金今年年初我花了三個月的業余時間用 Laravel 開發了一個項目,在此之前,除了去年換工作準備面試時,我并...
摘要:阻塞,非阻塞首先,阻塞這個詞來自操作系統的線程進程的狀態模型網絡爬蟲基本原理一后端掘金網絡爬蟲是捜索引擎抓取系統的重要組成部分。每門主要編程語言現未來已到后端掘金使用和在相同環境各加載多張小圖片,性能相差一倍。 2016 年度小結(服務器端方向)| 掘金技術征文 - 后端 - 掘金今年年初我花了三個月的業余時間用 Laravel 開發了一個項目,在此之前,除了去年換工作準備面試時,我并...
摘要:外部存儲器可用于長期保存大量程序和數據,其成本低容量大,但速度較慢。 1_計算機概述(了解) A:什么是計算機?計算機在生活中的應用舉例 計算機(Computer)全稱:電子計算機,俗稱電腦。是一種能夠按照程序運行,自動、高速處理海量數據的現代化智能電子設備。由硬件和軟件所組成,沒有安裝任何軟件的計算機稱為裸機。常見的形式有臺式計算機、筆記本計算機、大型計算機等。 應用舉例 ...
閱讀 3745·2023-04-25 18:41
閱讀 1178·2021-11-11 16:55
閱讀 1832·2021-09-22 15:54
閱讀 3075·2021-09-22 15:51
閱讀 3548·2019-08-30 15:55
閱讀 1944·2019-08-30 14:19
閱讀 1283·2019-08-29 10:57
閱讀 1704·2019-08-29 10:56