1. 程式人生 > >《深入理解java虛擬機器》筆記——簡析java類檔案結構

《深入理解java虛擬機器》筆記——簡析java類檔案結構

  一直不太搞得明白jvm到底是如何進行類載入的,在看資料的過程中迷迷糊糊,在理解類載入之前,首先看看java的類檔案結構到底是怎樣的,都包含了哪些內容。

  我寫了一個最簡單的java程式,根據這個程式來分析一下.class檔案中到底都存了些什麼。

java程式:

class Par {

    public int x = 5;
    public void f(){
        System.out.println("par static");
    }
}
class Sub extends Par{
    public int x = 6;
    public
void f() { System.out.println("Sub static"); } } public class Test { public static void main(String[] args) { Par par = new Sub(); System.out.println(par.x); par.f(); } }

生成的.class檔案內容

cafe babe 0000 0033 0027 0a00 0900 1207
0013 0a00 0200 1209 0014 0015 0900 1600
170a 0018 0019 0a00 1600 1
a07 001b 0700 1c01 0006 3c69 6e69 743e 0100 0328 2956 0100 0443 6f64 6501 000f 4c69 6e65 4e75 6d62 6572 5461 626c 6501 0004 6d61 696e 0100 1628 5b4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 5601 000a 536f 7572 6365 4669 6c65 0100 0954 6573 742e 6a61 7661 0c00 0a00 0b01 0016 636f 6d2f 7468 696e 6b69 6e67 696e 6a61 7661 2f53 7562 0700 1d0c 001e 001f 0700
200c 0021 0022 0700 230c 0024 0025 0c00 2600 0b01 0017 636f 6d2f 7468 696e 6b69 6e67 696e 6a61 7661 2f54 6573 7401 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 0100 106a 6176 612f 6c61 6e67 2f53 7973 7465 6d01 0003 6f75 7401 0015 4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b01 0016 636f 6d2f 7468 696e 6b69 6e67 696e 6a61 7661 2f50 6172 0100 0178 0100 0149 0100 136a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 0100 0770 7269 6e74 6c6e 0100 0428 4929 5601 0001 6600 2100 0800 0900 0000 0000 0200 0100 0a00 0b00 0100 0c00 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000d 0000 0006 0001 0000 0018 0009 000e 000f 0001 000c 0000 003b 0002 0002 0000 0017 bb00 0259 b700 034c b200 042b b400 05b6 0006 2bb6 0007 b100 0000 0100 0d00 0000 1200 0400 0000 1b00 0800 1c00 1200 1d00 1600 1e00 0100 1000 0000 0200 11

使用javap反彙編一下生成的.class檔案(javap -v com.thinkinginjava.Test)

Classfile /E:/Workspaces/MyEclipse/websocketTest/src/com/thinkinginjava/Test.cla
ss
  Last modified 2016-4-2; size 513 bytes
  MD5 checksum 32ca9f73cf634398be1085454c6b21c3
  Compiled from "Test.java"
public class com.thinkinginjava.Test
  SourceFile: "Test.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #9.#18         //  java/lang/Object."<init>":()V
   #2 = Class              #19            //  com/thinkinginjava/Sub
   #3 = Methodref          #2.#18         //  com/thinkinginjava/Sub."<init>":()V
   #4 = Fieldref           #20.#21        //  java/lang/System.out:Ljava/io/PrintStream;
   #5 = Fieldref           #22.#23        //  com/thinkinginjava/Par.x:I
   #6 = Methodref          #24.#25        //  java/io/PrintStream.println:(I)V
   #7 = Methodref          #22.#26        //  com/thinkinginjava/Par.f:()V
   #8 = Class              #27            //  com/thinkinginjava/Test
   #9 = Class              #28            //  java/lang/Object
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               SourceFile
  #17 = Utf8               Test.java
  #18 = NameAndType        #10:#11        //  "<init>":()V
  #19 = Utf8               com/thinkinginjava/Sub
  #20 = Class              #29            //  java/lang/System
  #21 = NameAndType        #30:#31        //  out:Ljava/io/PrintStream;
  #22 = Class              #32            //  com/thinkinginjava/Par
  #23 = NameAndType        #33:#34        //  x:I
  #24 = Class              #35            //  java/io/PrintStream
  #25 = NameAndType        #36:#37        //  println:(I)V
  #26 = NameAndType        #38:#11        //  f:()V
  #27 = Utf8               com/thinkinginjava/Test
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               com/thinkinginjava/Par
  #33 = Utf8               x
  #34 = Utf8               I
  #35 = Utf8               java/io/PrintStream
  #36 = Utf8               println
  #37 = Utf8               (I)V
  #38 = Utf8               f
{
  public com.thinkinginjava.Test();
    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 24: 0

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class com/thinkinginjava/Sub
         3: dup
         4: invokespecial #3                  // Method com/thinkinginjava/Sub."<init>":()V
         7: astore_1
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_1
        12: getfield      #5                  // Field com/thinkinginjava/Par.x:I
        15: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        18: aload_1
        19: invokevirtual #7                  // Method com/thinkinginjava/Par.f:()V
        22: return
      LineNumberTable:
        line 27: 0
        line 28: 8
        line 29: 18
        line 30: 22
}

  根據java虛擬機器規範,首先介紹一下Class 檔案格式

  “Class檔案格式採用一種類似於C語言結構體的偽結構來儲存資料,這種偽結構只有兩種資料型別:無符號數和表。”
  無符號數屬於基本的資料結構,以u1,u2,u4,u8來分別代表1個位元組,2個位元組,4個位元組和8個位元組的無符號數;
  表是由多個無符號數或者其他表作為資料項構成的複合資料型別,表都習慣性的以“_info”結尾。

Class檔案結構

官方文件:

ClassFile {
    u4             magic;//魔數
    u2             minor_version;//次版本號
    u2             major_version;//主版本號
    u2             constant_pool_count;//常量池容量計數
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;//訪問標誌
    u2             this_class;//類索引
    u2             super_class;//父類索引
    u2             interfaces_count;//介面索引數
    u2             interfaces[interfaces_count];//介面索引集合
    u2             fields_count;//欄位數
    field_info     fields[fields_count];
    u2             methods_count;//方法數
    method_info    methods[methods_count];
    u2             attributes_count;//屬性數
    attribute_info attributes[attributes_count];
}

一個個來分析:

魔數、主次版本號

這裡寫圖片描述

我本地的是JDK 1.7.0的,所以是主次版本號是 00 00 00 33,不同的jdk版本生成的版本號不同。

常量池

  在主次版本號後面的就是常量池入口,由於常量池的數量是不固定的,所以在常量池的入口需要放置一項u2型別的資料,代表常量池容量計數值
這裡寫圖片描述

  可見當前的計數值為0x0027 ,即39,這代表常量池中有38項常量,索引號為1-38,Class檔案只有常量池的容器技術是從1開始的
  從之前的反彙編程式碼中也可以看到,常量池中確實有38項
  
這裡寫圖片描述

  常量池中主要有3類常量:

  • 類和介面的全限定名
  • 欄位的名稱和描述符

每一種常量都有它的結構,例如:

CONSTANT_Class_info {
    u1 tag; //標誌,佔1個位元組 07
    u2 name_index;//指向全限定名常量項的索引
}
CONSTANT_Utf8_info {
    u1 tag;//標誌 01
    u2 length;//UTF-8編碼的字串佔用的位元組數
    u1 bytes[length];//長度為length的UTF-8編碼的字串
}
。。。。每一項可具體參考官方文件

舉幾個例子:

CONSTANT_MethodHandle_info {
    u1 tag; //標誌 10
    u1 reference_kind; //指向宣告方法的類描述符的CONSTANT_Class_info的索引項
    u2 reference_index;//指向名稱及型別描述符CONSTANT_NameAndType的索引項
}

tag:0a
reference_kind:0x0009
reference_index:0x0012
這裡寫圖片描述

CONSTANT_Utf8_info {
        u1 tag;//標誌 01
        u2 length;//UTF-8編碼的字串佔用的位元組數
        u1 bytes[length];//長度為length的UTF-8編碼的字串
    }

tag: 01
length: 6
bytes[length]:具體對應的編碼 3c 69 6e 69 74 3e
這裡寫圖片描述

可以看到,常量區總共有38項,在.class檔案中的表示如下圖所示,按照上面的分析即可分析出每個常量對應的是哪些16進位制位
這裡寫圖片描述

訪問標記

  分析完常量池,在常量池結束後,緊跟著的兩個位元組代表訪問標誌,這個標誌用於識別一些類或者介面層次的訪問資訊。
  (直接截圖了。。。)
這裡寫圖片描述

  從程式碼中可以分析出,我寫的java程式的訪問標誌位0x0021,即ACC_PUBLIC、ACC_SUPER標誌位真,其餘標誌為假,因此access_flags的值為0x0001 | 0x0020 = 0x0021(或運算即可算出是哪幾個標誌為真,哪幾個為假),如下圖所示。

這裡寫圖片描述

類索引、父類索引與介面索引集合

  類索引、父類索引和介面索引集合都按順序排列在訪問標記之後。

  類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,介面索引集合是一組u2型別的集合。

  • 類索引用於確定這個類的全限定名
  • 父類索引用於確定這個類的父類的全限定名

     對於介面索引集合,入口第一項u2型別的資料為介面計數器(interface_count),表示索引表的容量。,沒有實現任何介面,則計數為0,後面介面的索引表不再佔用任何位元組。

     如下圖所示,類索引對應0x0008,即com/thinkinginjava/Test類,父類索引為Object類,0x0009即指向java/lang/Object

這裡寫圖片描述

欄位表集合

  欄位表用於標書介面或者類中宣告的變數。欄位包括類級變數以及例項級變數,但不包括在方法內部宣告的區域性變數。
  
  java中描述一個欄位包含的資訊有:

  • 欄位的作用域(public,protected,private)
  • 例項變數還是類變數(static)
  • 可變性(final)
  • 併發可見性(volatile)
  • 可否序列化(transient)
  • 欄位資料型別(基本型別,物件,陣列)
  • 欄位名稱

    關於欄位表可以參考這篇博文看看
    因為我的程式中Test類中沒有定義欄位,所以結果顯示為0x0000,如下圖

    這裡寫圖片描述

方法表集合

  緊接著欄位表的是方法表集合,理解了欄位表看方法表就相對簡單了
  方法表的結構與欄位表一樣,依次包括了訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)幾項
官方文件

method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

方法訪問標誌:
這裡寫圖片描述

分析一下位元組碼:

  • 方法表集合的計數器值為0x0002,代表集合中有兩個方法(一個是編譯去新增的例項構造器<init>和原始碼中的main方法)
  • 第一個方法的訪問標誌為0x0001,即只有ACC_PUBLIC標誌為真
  • 名稱索引值為0x000a,查詢常量池可得方法名為<init>
  • 描述符索引值為0x000b,對應常量為”()V”
  • 屬性表計數器值為0x0001,表示有一個屬性,對應的屬性名稱索引為0x000c,對應常量為“Code”,說明此屬性時方法的位元組碼描述。

這裡寫圖片描述

屬性表集合

官方文件
  屬性表在之前出現過多次了,在Class檔案、欄位表、方法表中都可以攜帶自己的屬性表集合,用於描述某些場景專有的資訊。
  對於每個屬性,它的名稱需要從一個常量池中引用一個CONSTANT_Utf8_info型別的常量來表示,而屬性值的結構則是完全自定義的,只需要通過一個u4的長度屬性去說明屬性值所佔用的位數即可。

屬性表結構如下所示:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

舉幾個例子:
1.Code屬性
java方法體中的程式碼經過javac編譯處理後,最終會變成位元組碼指令儲存在Code屬性內。Code屬性出現在方法表的屬性集合之中,但並非所有的方法表都必須存在這個屬性,譬如介面或者抽象類中的方法就不存在Code屬性,如果Code屬性存在,那麼它的結構如下所示:

Code_attribute {
    u2 attribute_name_index; //指向CONSTANT_Utf8_info型常量的索引
    u4 attribute_length;     //屬性值的長度
    u2 max_stack;            //運算元棧深度的最大值
    u2 max_locals;           //區域性變量表所需的儲存空間
    //code_length和code用來儲存java源程式編譯後生成的位元組碼指令
    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];
}

分析一下位元組碼:

  • 由上面方法表的分析,第一個<init>方法對應的屬性名為Code,其值為0x000c
  • 緊接著為attribute_length,表示屬性值的長度,其值為0x0000001d
  • 接著為max_stack,表示運算元棧深度的最大值,其值為0x0001
  • 接著為max_locals, 表示區域性變量表所需的儲存空間,其值為0x0001
  • 接著為code_length,佔4個位元組,表示位元組碼長度,其值為0x00000005
  • 緊接著讀入跟隨的5個位元組,根據位元組碼指令表翻譯出對應的位元組碼指令。翻譯“2a b7 00 01 b1”過程:
    1)讀入2a,對用指令aload_0
    2)讀入b7,對應指令invokespecial
    3)讀入00 01,這是invokespecial的引數,為常量池中0x0001對應的常量
    4)讀入b1,對應的指令為return

這裡寫圖片描述

  • 接著為exception_table_length,此處為0,沒有異常
  • 接著為attribute_count,代表這個Code中有1個屬性
  • 然後就是這個屬性的描述啦,索引號為0x000d,找到對應的常量池索引號,為LineNumberTable,so,這個屬性是LineNumberTable,那麼看一下LineNumberTable屬性結構:
    LineNumberTable屬性

     LineNumberTable_attribute {
        u2 attribute_name_index;            //屬性名索引
        u4 attribute_length;                //屬性長度
        u2 line_number_table_length;
        {   u2 start_pc;                 //位元組碼行號
            u2 line_number;              //java原始碼行號
        } line_number_table[line_number_table_length];
    }
    

    從class檔案中可以看出,
    attribute_name_index為0x000d,
    attribute_length為0x00000006,
    line_number_table_length為0x0001,
    start_pc為0x0001
    line_number為0x0018
    正好對應了

      LineNumberTable:
        line 24: 0
    

    這裡寫圖片描述

    到此為止,一個<init>方法分析完了,包括了它包含的屬性。
    之前說了,該程式共有2個方法,還有一個為main函式,對它的分析跟上面一樣,不做分析了

這裡寫圖片描述