Java字節碼詳解(一) class文件結構

知識背景:JVM(Java Virtual Machine)是不能直接運行java的源碼文件的,必須要經過編譯變成字節碼文件才能運行。

java代碼運行過程

計算機是不能直接運行java代碼的,必須要先運行java虛擬機,再由java虛擬機運行編譯後的java代碼。這個編譯後的java代碼,就是本文要介紹的java字節碼。

爲什麼jvm不能直接運行java代碼呢,這是因爲在cpu層面看來計算機中所有的操作都是一個個指令的運行彙集而成的,java是高級語言,只有人類才能理解其邏輯,計算機是無法識別的,所以java代碼必須要先編譯成字節碼文件,jvm才能正確識別代碼轉換後的指令並將其運行。

字節碼文件的結構

class文件本質上是一個以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊的排列在class文件中。jvm根據其特定的規則解析該二進制數據,從而得到相關信息。

Class文件採用一種僞結構來存儲數據,它有兩種類型:無符號數。這裏暫不詳細的講。

下圖是接下來本文的簡單的java例子編譯後的文件,已經將class文件轉爲16進制。可以看到,我們熟悉的java代碼經過編譯轉換爲只有機器能識別的數據。
在這裏插入圖片描述

1.1 Class文件的結構屬性

我們先從整體看下java字節碼文件包含了哪些類型的數據:
在這裏插入圖片描述

1.2 用一個簡單的示例代碼分析

新建一個Test.java文件,然後編輯

public class Test{
	public static void main(String[] args){
		Integer a=1;
		Integer b=2;
		Integer c=a+b;	
		System.out.println(c+"");
	}
}

將該代碼進行編譯並保存在當前目錄下:
Alt
編譯完成目錄下會多一個Test.class編譯文件。使用16進制打開,內容如下:
在這裏插入圖片描述

在圖中,前4個字節cafe babe就是魔數,緊接着魔數的4個字節代表的是Class文件的版本號。

第5,6個字節表示的是次版本號(minor version),在上圖中爲0000,說明class文件的次版本號爲 0 。

第7,8個字節代表主版本號(major version),在上圖中爲0034,因爲是16進制,計算可以得到該class文件的主版本號爲52.

以此類推,根據java字節碼的規則,可以依次解析成該字節碼文件的所有內容。

當然,jdk中包含了一個可以將字節碼文件“可視化”操作的命令javap,運行該命令,可以將java字節碼文件解析爲符合人類邏輯的文件。

查看java字節碼可以使用 javap 命令,當然也可以使用IDE的插件進行查看(比如在Idea中使用jclasslib插件進行查看)

在當前目錄下運行

javap -v Test.class

會出現我們可以正常閱讀的字節碼

Classfile /F:/Test.class                                                                                            
  Last modified 2018-10-21; size 752 bytes                                                                          
  MD5 checksum 4848a65fcbc8b0bc8ce60eb32172471b                                                                     
  Compiled from "Test.java"                                                                                         
public class Test                                                                                                   
  minor version: 0                                                                                                  
  major version: 52                                                                                                 
  flags: ACC_PUBLIC, ACC_SUPER                                                                                      
Constant pool:                                                                                                      
   #1 = Methodref          #13.#22        // java/lang/Object."<init>":()V                                          
       *** 省略部分代碼                                                     
  #13 = Class              #36            // java/lang/Object                                                       
  #14 = Utf8               <init>                                                                                   
  #15 = Utf8               ()V                                                                                      
        *** 省略部分代碼                                                     
  #22 = NameAndType        #14:#15        // "<init>":()V                                                           
        *** 省略部分代碼                                                                   
  #36 = Utf8               java/lang/Object                                                                         
        *** 省略部分代碼                                           
{                                                                                                                   
  public Test();                                                                                                    
    descriptor: ()V                                                                                                 
    flags: ACC_PUBLIC                                                                                               
    Code:                                                                                                           
      stack=1, locals=1, args_size=1                                                                                
         0: aload_0                                                                                                 
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V                               
         4: return                                                                                                  
      LineNumberTable:                                                                                              
        line 1: 0                                                                                                   
                                                                                                                    
  public static void main(java.lang.String[]);                                                                      
    descriptor: ([Ljava/lang/String;)V                                                                              
    flags: ACC_PUBLIC, ACC_STATIC                                                                                   
    Code:                                                                                                           
      stack=3, locals=4, args_size=1                                                                                
         0: iconst_1                                                                                                
         1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;            
         4: astore_1                                                                                                
         5: iconst_2                                                                                                
         6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;            
         9: astore_2                                                                                                
        10: aload_1                                                                                                 
        11: invokevirtual #3                  // Method java/lang/Integer.intValue:()I                              
        14: aload_2                                                                                                 
        15: invokevirtual #3                  // Method java/lang/Integer.intValue:()I                              
        18: iadd                                                                                                    
        19: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;            
        22: astore_3                                                                                                
        23: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;                   
        26: new           #5                  // class java/lang/StringBuilder                                      
        29: dup                                                                                                     
        30: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V                        
        33: aload_3                                                                                                 
        34: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lan
/StringBuilder;                                                                                                     
        37: ldc           #8                  // String                                                             
        39: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lan
/StringBuilder;                                                                                                     
        42: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;       
        45: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V           
        48: return                                                                                                  
      LineNumberTable:                                                                                              
        line 3: 0                                                                                                   
        line 4: 5                                                                                                   
        line 5: 10                                                                                                  
        line 6: 23                                                                                                  
        line 7: 48                                                                                                  
}                                                                                                                   
SourceFile: "Test.java"                                                                                                                                                                                          

1.3 分析由 javap -v 命令顯示的字節碼文件

我們從上到下看看例子的字節碼文件各個字段代表的意思

字節碼文件屬性

Classfile /F:/Test.class                                                                                            
  Last modified 2018-10-21; size 752 bytes                                                                          
  MD5 checksum 4848a65fcbc8b0bc8ce60eb32172471b                                                                     
  Compiled from "Test.java"  

這一部分一看就很明白

  • Last modified 最後修改時間
  • size: 該字節碼文件大小
  • MD5 checksum 該文件的md5
  • Compiled from "Test.java"表示該字節碼文件是由“Test.java”編譯而來

類屬性

public class Test                                                                                                   
  minor version: 0                                                                                                  
  major version: 52                                                                                                 
  flags: ACC_PUBLIC, ACC_SUPER          

這一部分表示Test類的各個屬性

  • minor version: 0 表示可以支持最小的jdk版本,java的jdk都是向下兼容的,所以該值一般都爲0
  • major version: 52 編譯Test.java的jdk版本,我使用的是jdk1.8,所以52代表的是jdk8
  • flags: ACC_PUBLIC, ACC_SUPER 表示該類的訪問屬性

flags屬性類型及含義如下

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 是否爲Public類型
ACC_FINAL 0x0010 是否被聲明爲final,只有類可以設置
ACC_SUPER 0x0020 是否允許使用invokespecial字節碼指令的新語義.
ACC_INTERFACE 0x0200 標誌這是一個接口
ACC_ABSTRACT 0x0400 是否爲abstract類型,對於接口或者抽象類來說,次標誌值爲真,其他類型爲假
ACC_SYNTHETIC 0x1000 標誌這個類並非由用戶代碼產生
ACC_ANNOTATION 0x2000 標誌這是一個註解
ACC_ENUM 0x4000 標誌這是一個枚舉

常量池

Constant pool:                                                                                                      
   #1 = Methodref          #13.#22        // java/lang/Object."<init>":()V                                          
       *** 省略部分代碼                                                     
  #13 = Class              #36            // java/lang/Object                                                       
  #14 = Utf8               <init>                                                                                   
  #15 = Utf8               ()V                                                                                      
        *** 省略部分代碼                                                     
  #22 = NameAndType        #14:#15        // "<init>":()V                                                           
        *** 省略部分代碼                                                                   
  #36 = Utf8               java/lang/Object                                                                         
        *** 省略部分代碼    

常量池,可以理解爲java資源的倉庫,JVM運行方法時需要用到的數據都會在這裏拿。下面從第一個常量開始分析各個字符的意義:

#1 = Methodref #13.#22 // java/lang/Object."<init>":()V

  1. #1 表示常量池值的序號,該數字表示爲第一個
  2. #1 = Methodref 表示第一個值是一個方法的引用
  3. #13.#22 表示該方法的完整屬性還需要 #13#22的來複合表示

#13 = Class #36 // java/lang/Object
#36 = Utf8 java/lang/Object

  1. #13 說明屬性爲class,而#36指明瞭該class爲object類
  2. #1336結合一起,#13就得屬性就是一個class類,且其具體的類爲java.lang.Object
  3. 仔細觀察 #13 後的註釋,你發現了什麼?其實在將java字節碼可視化的時候,javap就已經將各個常量的屬性都給關聯好了

#22 = NameAndType #14:#15 // "<init>":()V

  1. NameAndType 這一看知道是代表名字和類型
  2. 按照上述的方式解析,會發現#22的名字爲 <init>,類型爲()V表示無參數並且返回值爲void

根據上面的步驟分析可以得出:#1 引用的方法就是Test類的初始構造方法

方法表集合

  public Test();                                                                                                    
    descriptor: ()V                                                                                                 
    flags: ACC_PUBLIC                                                                                               
    Code:                                                                                                           
      stack=1, locals=1, args_size=1                                                                                
         0: aload_0                                                                                                 
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V                               
         4: return                                                                                                  
      LineNumberTable:                                                                                              
        line 1: 0    

這個Test()方法就是我們Test類中默認的構造方法,在我們沒有重寫或者沒有指定對應的構造方法時,java編譯的時候會默認生成一個空的構造方法。
下面我們來看看這個方法裏面的各個字段代表的意思:
descriptor: ()V

·descriptor· 表示該方法的描述,這裏表示的是該方法的參數爲空,且方法值爲void

Code:                                                                                                           
      stack=1, locals=1, args_size=1                                                                                
         0: aload_0                                                                                                 
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V                               
         4: return  
  1. stack 最大操作數棧,JVM運行時會根據這個值來分配棧幀(Frame)中的操作棧深度,此處爲1
  2. locals 局部變量所需的存儲空間,單位爲Slot, Slot是虛擬機爲局部變量分配內存時所使用的最小單位,爲4個字節大小。方法參數(包括實例方法中的隱藏參數this),顯示異常處理器的參數(try catch中的catch塊所定義的異常),方法體中定義的局部變量都需要使用局部變量表來存放。值得一提的是,locals的大小並不一定等於所有局部變量所佔的Slot之和,因爲局部變量中的Slot是可以重用的。
  3. args_size方法參數的個數,這裏是1,因爲每個實例方法都會有一個隱藏參數this
  4. 0: aload_0
    1: invokespecial #1 // Method java/lang/Object."<init>":()V
    4: return
    表示運行方法時,各個指令的順序,該順序在下一章會進行詳細的介紹。
  5. LineNumberTable 該屬性的作用是描述幀棧中局部變量與源碼中定義的變量之間的關係。可以使用 -g:none 或 -g:vars來取消或生成這項信息,如果沒有生成這項信息,那麼當別人引用這個方法時,將無法獲取到參數名稱,取而代之的是arg0, arg1這樣的佔位符。
    start 表示該局部變量在哪一行開始可見,length表示可見行數,Slot代表所在幀棧位置,Name是變量名稱,然後是類型簽名。

總結

至此,把java字節碼文件的各個屬性都簡單的介紹了一次。當然,還有很多其他的屬性沒有在示例代碼中體現出來,這裏就不進一步的介紹。本篇文章目的讓大家對java字節碼有個清晰的認識,在大腦中有個基礎的概念,以後如果想深入瞭解,就知道從哪一方面入手,進行快速的學習。

參考文章:
深入理解JVM-字節碼詳解
輕鬆看懂字節碼文件
詳解java字節碼class文件
《深入理解java虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章