Java語言最廣爲人知的口號就是「一次編譯處處運行」,這裏的「編譯」指的是編譯器將Java源代碼編譯爲Java字節碼文件(也就是.class文件,本文中不作區分),「運行」則指的是Java虛擬機執行字節碼文件。Java的跨平臺得益於不一樣平臺上不一樣的JVM的實現,只要提供規範的字節碼文件,不管是什麼平臺的JVM都可以執行,這樣字節碼文件就作到了處處運行。這篇文章將經過一個簡單的實例來分析字節碼的結構,加深對Java程序運行機制的理解。html
第一步,咱們要準備一個字節碼文件。先寫一個簡單的Java源程序TestByteCode.java
:java
package com.sinosun.test;
public class TestByteCode{
private int a = 1;
public String b = "2";
protected void method1(){}
public int method2(){
return this.a;
}
private String method3(){
return this.b;
}
}
複製代碼
使用javac
命令將上面的代碼進行編譯,獲得對應的TestByteCode.class文件,到這裏就完成了第一步。git
通過上一步已經獲得了TestByteCode.class文件,也就是咱們須要的字節碼。咱們不妨先來看一下文件的內容。(注意IDEA打開.class文件時會自動進行反編譯,這裏使用IDEA中的HexView插件查看.class文件,也可使用Sublime Text直接打開.class文件)能夠看到字節碼文件中是一大堆16進制字節,下圖中紅色框中的部分就是.class文件中的真實內容:github
要想理解class文件,必須先知道它的組成結構。按照JVM的字節碼規範,一個典型的class文件由十個部分組成:MagicNumber、Version、Constant_Pool、Access_flag、This_class、Super_class、Interface、Fields、Method以及Attributes。字節碼中包括兩種數據類型:無符號數和表。無符號數又包括 u1,u2,u4,u8四種,分別表明1個字節、2個字節、4個字節和8個字節。而表結構則是由無符號數據組成的。segmentfault
根據規定,一個字節碼文件的格式固定以下:數組
根據上表能夠清晰地看出,字節碼採用固定的文件結構和數據類型來實現對內容的分割,結構很是緊湊,沒有任何冗餘的信息,連分隔符都沒有。bash
根據結構表,.class文件的前四個字節存放的內容就是.class文件的魔數(magic number)。魔數是一個固定值:0xcafebabe
,也是JVM識別.class文件的標誌。咱們一般是根據後綴名來區分文件類型的,可是後綴名是能夠任意修改的,所以虛擬機在加載類文件以前會先檢查這四個字節,若是不是0xcafebabe
則拒絕加載該文件。ide
關於魔數爲何是0xcafebabe
,請移步DZone圍觀James Gosling的解釋。工具
版本號緊跟在魔數以後,由兩個2字節的字段組成,分別表示當前.class文件的主版本號和次版本號,版本號數字與實際JDK版本的對應關係以下圖。編譯生成.class文件的版本號與編譯時使用的-target參數有關。ui
編譯器版本 | -target參數 | 十六進制表示 | 十進制表示 |
---|---|---|---|
JDK 1.6.0_01 | 不帶(默認 -target 1.6) | 00 00 00 32 | 50 |
JDK 1.6.0_01 | -target 1.5 | 00 00 00 31 | 49 |
JDK 1.6.0_01 | -target 1.4 -source 1.4 | 00 00 00 30 | 48 |
JDK 1.7.0 | 不帶(默認 -target 1.6) | 00 00 00 32 | 50 |
JDK 1.7.0 | -target 1.7 | 00 00 00 33 | 51 |
JDK 1.7.0 | -target 1.4 -source 1.4 | 00 00 00 30 | 48 |
JDK 1.8.0 | 無-target參數 | 00 00 00 34 | 52 |
第二節中獲得的.class文件中,魔數對應的值爲:0x0000 0034
,表示對應的JDK版本爲1.8.0,與編譯時使用的JDK版本一致。
常量池是解析.class文件的重點之一,首先看常量池中對象的數量。根據第二節可知,constant_pool_count
的值爲0x001c
,轉換爲十進制爲28,根據JVM規範,constant_pool_count
的值等於constant_pool
中的條目數加1,所以,常量池中共有27個常量。
根據JVM規範,常量池中的常量的通常格式以下:
cp_info {
u1 tag;
u1 info[];
}
複製代碼
共有11種類型的數據常量,各自的tag和內容以下表所示:
咱們經過例子來查看如何分析常量,下圖中,紅線部分爲常量池的部份內容。
首先第一個tag值爲0x0a
,查看上面的表格可知該常量對應的是CONSTANT_Methodref_info
,即指向一個方法的引用。tag後面的兩個2字節分別指向常量池中的一個CONSTANT_Class_info型常量和一個CONSTANT_NameAndType_info型常量,該常量的完整數據爲:0a 0006 0016
,兩個索引常量池中的第6個常量和第22個常量,根據上表能夠知道其含義爲:
0a 0006 0016 Methodref class#6 nameAndType#22
由於還未解析第6個及第22個常量,這裏先使用佔位符代替。
同理能夠解析出其它的常量,分析獲得的完整常量池以下:
序號 | 16進製表示 | 含義 | 常量值 |
---|---|---|---|
1 | 0a 0006 0016 | Methodref #6 #22 | java/lang/Object."":()V |
2 | 09 0005 0017 | Fieldref #5 #23 | com/sinosun/test/TestByteCode.a:I |
3 | 08 0018 | String #24 | 2 |
4 | 09 0005 0019 | Fieldref #5 #25 | com/sinosun/test/TestByteCode.b:Ljava/lang/String; |
5 | 07 001a | Class #26 | com/sinosun/test/TestByteCode |
6 | 07 001b | Class #27 | java/lang/Object |
7 | 01 0001 61 | UTF8編碼 | a |
8 | 01 0001 49 | UTF8編碼 | I |
9 | 01 0001 62 | UTF8編碼 | b |
10 | 01 0012 4c6a6176612f6c616e672f537472696e673b | UTF8編碼 | Ljava/lang/String; |
11 | 01 0006 3c 69 6e 69 74 3e | UTF8編碼 | |
12 | 01 0003 28 29 56 | UTF8編碼 | ()V |
13 | 01 0004 43 6f 64 65 | UTF8編碼 | Code |
14 | 01 000f 4c696e654e756d6265725461626c65 | UTF8編碼 | LineNumberTable |
15 | 01 0007 6d 65 74 68 6f 64 31 | UTF8編碼 | method1 |
16 | 01 0007 6d 65 74 68 6f 64 32 | UTF8編碼 | method2 |
17 | 01 0003 28 29 49 | UTF8編碼 | ()I |
18 | 01 0007 6d 65 74 68 6f 64 33 | UTF8編碼 | method3 |
19 | 01 0014 28294c6a6176612f6c616e672f537472696e673b | UTF8編碼 | ()Ljava/lang/String; |
20 | 01 000a 53 6f 75 72 63 65 46 69 6c 65 | UTF8編碼 | SourceFile |
21 | 01 0011 5465737442797465436f64652e6a617661 | UTF8編碼 | TestByteCode.java |
22 | 0c 000b 000c | NameAndType #11 #12 | "":()V |
23 | 0c 0007 0008 | NameAndType #7 #8 | a:I |
24 | 01 0001 32 | UTF8編碼 | 2 |
25 | 0c 0009 000a | NameAndType #9 #10 | b:Ljava/lang/String; |
26 | 01 001d 636f6d2f73696e6f73756e2f746573 742f5465737442797465436f6465 | UTF8編碼 | com/sinosun/test/TestByteCode |
27 | 01 0010 6a6176612f6c616e672f4f626a656374 | UTF8編碼 | java/lang/Object |
上表所示即爲常量池中解析出的全部常量,關於這些常量的用法會在後文進行解釋。
access_flag
標識的是當前.class文件的訪問權限和屬性。根據下表能夠看出,該標誌包含的信息包括該class文件是類仍是接口,外部訪問權限,是不是abstract
,若是是類的話,是否被聲明爲final
等等。
Flag Name | Value | Remarks |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_SUPER | 0x0020 | 用於兼容早期編譯器,新編譯器都設置該標記,以在使用 invokespecial 指令時對子類方法作特定處理。 |
ACC_INTERFACE | 0x0200 | 接口,同時須要設置:ACC_ABSTRACT。不可同時設置:ACC_FINAL、ACC_SUPER、ACC_ENUM |
ACC_ABSTRACT | 0x0400 | 抽象類,沒法實例化。不可與ACC_FINAL同時設置。 |
ACC_SYNTHETIC | 0x1000 | synthetic,由編譯器產生,不存在於源代碼中。 |
ACC_ANNOTATION | 0x2000 | 註解類型(annotation),需同時設置:ACC_INTERFACE、ACC_ABSTRACT |
ACC_ENUM | 0x4000 | 枚舉類型 |
本文的字節碼文件中access_flag
標誌的取值爲0021
,上表中沒法直接查詢到該值,由於access_flag
的值是一系列標誌位的並集,0x0021 = 0x0020+0x0001
,所以該類是public型的。
訪問標誌在後文的一些屬性中也會屢次使用。
類索引this_class
保存的是當前類的全限定名在常量池中的索引,取值爲0x0005
,指向常量池中的第5個常量,查表可知內容爲:com/sinosun/test/TestByteCode
。
父類索引super_class
保存的是當前類的父類的全侷限定名在常量池中的索引,取值爲0x0006
,指向池中的第6個常量,值爲:java/lang/Object
。
接口信息interfaces
保存了當前類實現的接口列表,包含接口數量和包含全部接口全侷限定名索引的數組。本文的示例代碼中沒有實現接口,所以數量爲0。
接下來解析字段Fields
部分,前兩個字節是fields_count
,值爲0x0002
,代表字段數量爲2。 其中每一個字段的結構用field_info
表示:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製代碼
根據該結構來分析兩個字段,第一個字段的內容爲0002 0007 0008 0000
,訪問標誌位0x0002
表示該字段是private
型的,名稱索引指向常量池中第7個值爲a
,類型描述符指向常量池中第8個值爲I
,關聯的屬性數量爲0,可知該字段爲private I a
,其中I
表示 int
。
一樣,經過0001 0009 000a 0000
能夠分析出第二個字段,其值爲public Ljava/lang/String; b
。其中的Ljava/lang/String;
表示String
。
關於字段描述符與源代碼的對應關係,下表是一個簡單的示意:
描述符 | 源代碼 |
---|---|
Ljava/lang/String; | String |
I | int |
[Ljava/lang/Object; | Object[] |
[Z | boolean[] |
[[Lcom/sinosun/generics/FileInfo; | com.sinosun.generics.FileInfo[][] |
字段結束後進入對方法methods
的解析,首先能夠看到方法的數量爲0x0004
,共四個。
不對啊!TestByteCode.java
中明明只有三個方法,爲何.class
文件中的方法數變成了4個?
由於編譯時自動生成了一個<init>
方法做爲類的默認構造方法。
接下來對每一個方法進行分析,老規矩,分析以前首先了解方法的格式定義:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製代碼
根據該格式,首先獲得第一個方法的前8個字節0001 000b 000c 0001
,對照上面的格式以及以前常量池和訪問標誌的內容,能夠知道該方法是:public <init> ()V
,且附帶一個屬性。能夠看到該方法名就是<init>
。對於方法附帶的屬性而言,有着以下格式:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
複製代碼
繼續分析後面的內容000d
,查詢常量池能夠知道該屬性的名稱爲:Code
。Code
屬性是method_info
屬性表中一種可變長度的屬性,該屬性中包括JVM指令及方法的輔助信息,如實例初始化方法或者類或接口的初始化方法。若是一個方法被聲明爲native
或者abstract
,那麼其method_info
結構中的屬性表中必定不包含Code
屬性。不然,其屬性表中一定包含一個Code
屬性。
Code屬性的格式定義以下:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製代碼
對照上面的結構分析字節序列000d 00000030 0002 0001
,該屬性爲Code
屬性,屬性包含的字節數爲0x00000030
,即48個字節,這裏的長度不包括名稱索引與長度這兩個字段。max_stack
表示方法運行時所能達到的操做數棧的最大深度,爲2;max_locals
表示方法執行過程當中建立的局部變量的數目,包含用來在方法執行時向其傳遞參數的局部變量。
接下來是一個方法真正的邏輯核心——字節碼指令,這些JVM指令是方法的真正實現。首先是code_length
表示code長度,這裏的值爲16,表示後面16個字節是指令內容,2a b7 0001 2a 04 b5 0002 2a 12 03 b5 0004 b1
。
爲了便於理解,將這些指令翻譯爲對應的助記符:
字節碼 | 助記符 | 指令含義 |
---|---|---|
0x2a | aload_0 | 將第一個引用類型本地變量推送至棧頂 |
0xb7 | invokespecial | 調用超類構建方法, 實例初始化方法, 私有方法 |
0x04 | iconst_1 | 將int型1推送至棧頂 |
0xb5 | putfield | 爲指定類的實例域賦值 |
0x12 | ldc | 將int,float或String型常量值從常量池中推送至棧頂 |
0xb1 | return | 從當前方法返回void |
對照表格能夠看出這幾個指令的含義爲:
2a aload_0
b7 0001 invokespecial #1 //Method java/lang/Object."":()V
2a aload_0
04 iconst_1
b5 0002 putfield #2 //Field a:I
2a aload_0
12 03 ldc #3 //String 2
b5 0004 putfield #4 //Field b:Ljava/lang/String;
b1 return
能夠看出,在初始化方法中,前後將類自身引用this_class、類中的變量a和變量b入棧,併爲兩個變量賦值,以後方法結束。
指令分析結束後,是方法中的異常表,本方法中未拋出任何異常,所以表長度爲0000
。後面的0001
表示後面有一個屬性。根據以前的屬性格式能夠知道,該屬性的名稱索引爲0x000e
,查找常量池可知該屬性爲LineNumberTable
屬性。
下面是LineNumberTable
屬性的結構:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
複製代碼
結合該結構,分析0000000e 0003 0000 0003 0004 0004 0009 0005
可知,該表中共有三項,第一個數字表示指令碼中的字節位置,第二個數字表示源代碼中的行數。
同理,能夠對後面的方法進行分析。
第二個方法,0004 000f 000c 0001
表示方法名及訪問控制符爲protected method1 ()V
,且附有一個屬性。000d 00000019
,毫無疑問,屬性就是Code
,長度爲25個字節。
0000 0001 00000001 b1
能夠看出操做數棧深度max_stack
爲0,max_locals
爲1表示有一個局部變量,全部方法默認都會有一個指向其所在類的參數。方法體中只有一個字節指令,就是return
,由於該方法是一個空方法。0000 0001
代表沒有異常,且附有一個屬性。000e 00000006 0001 0000 0007
屬性是LineNumberTable
,內容代表第一個字節指令與代碼的第7行對應。
在後面兩個方法中,使用了三個新的字節指令:
字節碼 | 助記符 | 指令含義 |
---|---|---|
0xb4 | getfield | 獲取指定類的實例域, 並將其壓入棧頂 |
0xac | ireturn | 從當前方法返回int |
0xb0 | areturn | 從當前方法返回對象引用 |
解析0001 0010 0011 0001 000d 0000 001d
可知第三個方法爲public method2 ()I
,其Code
屬性內容爲0001 0001 00000005 2a b4 0002 ac
, 獲取變量a
並返回。 後面仍然是異常信息和LineNumberTable
。
第四個方法這裏再也不贅述。
0002 0012 0013 0001 000d 0000 001d private method3 ()Ljava/lang/String;
Code
0001 0001 00000005
2a b4 0004 b0 獲取變量b並返回
0000
LineNumberTable
0001 000e 00000006 0001 0000 000e //line 14 : 0
這樣,咱們就在字節碼中解析出了類中的方法。字節指令是方法實現的核心,字節指令在任何一個JVM中都對應的是同樣的操做,所以字節碼文件能夠實現跨平臺運行。可是每個平臺中對字節指令的實現細節各有不一樣,這是Java程序在不一樣平臺間真正"跨"的一步。
最後一部分是該類的屬性Attributes
,數量爲0x0001
,根據attribute_info
來分析該屬性。
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
複製代碼
前兩個字節對應name_index
,爲0x0014
,即常量池中的第20個常量,查表獲得SourceFile
,說明該屬性是SourceFile
屬性。該屬性是類文件屬性表中的一個可選定長屬性,其結構以下:
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
複製代碼
獲得該屬性的所有內容爲0014 00000002 0015
,對比常量表可知內容爲「SourceFile ——TestByteCode.java」,也就是指定了該.class
文件對應的源代碼文件。
本文到此就算結束了,看到這裏的話應該對字節碼的結構有了基本的瞭解。
可是,前面花了這麼大篇幅所作的事情,Java早就提供了一個命令行工具javap
所有實現了,進入.class
文件所在的文件夾,打開命令行工具,鍵入以下命令:
javap -verbose XXX.class
複製代碼
結果以下所示:
PS E:\blog\Java字節碼\資料> javap -verbose TestByteCode.class
Classfile /E:/blog/Java字節碼/資料/TestByteCode.class
Last modified 2018-9-6; size 494 bytes
MD5 checksum 180292e6f6e8e9e48807195b235fa8ef
Compiled from "TestByteCode.java"
public class com.sinosun.test.TestByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#23 // com/sinosun/test/TestByteCode.a:I
#3 = String #24 // 2
#4 = Fieldref #5.#25 // com/sinosun/test/TestByteCode.b:Ljava/lang/String;
#5 = Class #26 // com/sinosun/test/TestByteCode
#6 = Class #27 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 b
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 method1
#16 = Utf8 method2
#17 = Utf8 ()I
#18 = Utf8 method3
#19 = Utf8 ()Ljava/lang/String;
#20 = Utf8 SourceFile
#21 = Utf8 TestByteCode.java
#22 = NameAndType #11:#12 // "<init>":()V
#23 = NameAndType #7:#8 // a:I
#24 = Utf8 2
#25 = NameAndType #9:#10 // b:Ljava/lang/String;
#26 = Utf8 com/sinosun/test/TestByteCode
#27 = Utf8 java/lang/Object
{
public java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public com.sinosun.test.TestByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: aload_0
10: ldc #3 // String 2
12: putfield #4 // Field b:Ljava/lang/String;
15: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 9
protected void method1();
descriptor: ()V
flags: ACC_PROTECTED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
public int method2();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 10: 0
}
SourceFile: "TestByteCode.java"
複製代碼
基本就是咱們以前解析獲得的結果。
固然,我分享這些過程的初衷並非但願本身或讀者變成反編譯工具,一眼看穿字節碼的真相。這些事情人不會作的比工具更好,可是理解這些東西能夠幫助咱們作出更好的工具,好比CGlib,就是經過在類加載以前添加某些操做或者直接動態的生成字節碼來實現動態代理,比使用java反射的JDK動態代理要快。
我總認爲,人應該好好利用工具,可是也應該對工具背後的細節懷有好奇心與探索欲。就這篇文章來講,若是能讓你們對字節碼多一些認識,那目的就已經達到了。括弧笑