實例分析理解Java字節碼

Java語言最廣爲人知的口號就是「一次編譯處處運行」,這裏的「編譯」指的是編譯器將Java源代碼編譯爲Java字節碼文件(也就是.class文件,本文中不作區分),「運行」則指的是Java虛擬機執行字節碼文件。Java的跨平臺得益於不一樣平臺上不一樣的JVM的實現,只要提供規範的字節碼文件,不管是什麼平臺的JVM都可以執行,這樣字節碼文件就作到了處處運行。這篇文章將經過一個簡單的實例來分析字節碼的結構,加深對Java程序運行機制的理解。html

一、 準備.class文件

第一步,咱們要準備一個字節碼文件。先寫一個簡單的Java源程序TestByteCode.javajava

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

二、 人工解析.class文件

通過上一步已經獲得了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和內容以下表所示:

常量結構

咱們經過例子來查看如何分析常量,下圖中,紅線部分爲常量池的部份內容。

常量池示例1

首先第一個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,查詢常量池能夠知道該屬性的名稱爲:CodeCode屬性是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動態代理要快。

我總認爲,人應該好好利用工具,可是也應該對工具背後的細節懷有好奇心與探索欲。就這篇文章來講,若是能讓你們對字節碼多一些認識,那目的就已經達到了。括弧笑

參考文章

  1. 一文讓你明白Java字節碼
  2. 深刻理解JVM之Java字節碼(.class)文件詳解
  3. [從字節碼層面看「HelloWorld」]
  4. JVM之字節碼——Class文件格式
  5. JavaCodeToByteCode
  6. JVM 虛擬機字節碼指令表
相關文章
相關標籤/搜索