Java編譯後的class文件解析

文章已同步github博客:Java編譯後的class文件解析

1、編譯Java類

1.1、寫Java類

編寫一份Java類,即.java文件,例如:

package com.jesus.util;

public class TestDemo {

    public static final String SUCCESS = "success";

    public static void main(String[] args) {
        System.out.println("main");
    }

    private String printText(String str) {
        System.out.println(str);
        return SUCCESS;
    }
}

1.2、編譯

通過javac命令編譯爲class文件:

javac DemoTest.java

2、.class文件結構

生成的.class文件如下:

cafe babe 0000 0034 0023 0a00 0700 1509
0016 0017 0800 0f0a 0018 0019 0700 1a08
001b 0700 1c01 0007 5355 4343 4553 5301
0012 4c6a 6176 612f 6c61 6e67 2f53 7472
696e 673b 0100 0d43 6f6e 7374 616e 7456
616c 7565 0100 063c 696e 6974 3e01 0003
2829 5601 0004 436f 6465 0100 0f4c 696e
654e 756d 6265 7254 6162 6c65 0100 046d
6169 6e01 0016 285b 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956 0100 0970
7269 6e74 5465 7874 0100 2628 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 294c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b01 000a 536f 7572 6365 4669 6c65 0100
0d54 6573 7444 656d 6f2e 6a61 7661 0c00
0b00 0c07 001d 0c00 1e00 1f07 0020 0c00
2100 2201 0017 636f 6d2f 6a65 7375 732f
7574 696c 2f54 6573 7444 656d 6f01 0007
7375 6363 6573 7301 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 0013 6a61
7661 2f69 6f2f 5072 696e 7453 7472 6561
6d01 0007 7072 696e 746c 6e01 0015 284c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b29 5600 2100 0500 0700 0000 0100 1900
0800 0900 0100 0a00 0000 0200 0600 0300
0100 0b00 0c00 0100 0d00 0000 1d00 0100
0100 0000 052a b700 01b1 0000 0001 000e
0000 0006 0001 0000 0003 0009 000f 0010
0001 000d 0000 0025 0002 0001 0000 0009
b200 0212 03b6 0004 b100 0000 0100 0e00
0000 0a00 0200 0000 0800 0800 0900 0200
1100 1200 0100 0d00 0000 2600 0200 0200
0000 0ab2 0002 2bb6 0004 1206 b000 0000
0100 0e00 0000 0a00 0200 0000 0c00 0700
0d00 0100 1300 0000 0200 14

2.1、.class文件組成

以上可知,.class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在.class文件之中,中間沒有添加任何分隔符;

根據Java虛擬機規範的規定,.class文件格式採用一種類似於C語言的僞結構來存儲數據,包含無符號數和表:

  • 無符號數:以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節和8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成的字符串值;
  • 表:由多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表都習慣性的以”_info“結尾。

Class文件的結構沒有分隔符,無論你是數量還是順序,都是嚴格規定的,哪個字節代表什麼含義、長度多少、先後順序如何,都不允許改變。

2.2、.class文件結構

ClassFile {
  u4             magic; // 魔數,固定值 0xCAFEBABE
  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]; // 屬性表
}

2.2.1、魔數

魔數(Magic Number),u4,即class文件中用4個字節表示,他的唯一作用就是確定這個文件是否爲一個能否被虛擬機所識別的class文件,魔數的固定值爲0xCAFEBABE,不會改變;

2.2.2、文件版本號

緊挨着魔數的2個字節爲副版本號minor_version(上面class文件例子中0x0000),接下來的2個字節爲主版本號major_version(上面例子中的0x0034,換算成10進製爲52,查詢jdk版本對應關係,主版本52爲jdk 1.8);

2.2.3、常量池計數器

接下來的2個字節表示常量池計數器constant_pool_count,常量池中的數量不固定,constant_pool_count的值 = 常量池中的成員數 + 1,常量池的索引從1開始;

例如上面class文件中的16進制數爲0x0023,換算成10進製爲35,通過命令“javap -v DemoTest.class"來查看常量池,上面class文件的常量池內容如下:

xxxMacBook-Pro:JVM_test xxx$ javap -v TestDemo.class 
Classfile /Users/xxx/xxx/TestDemo.class
  Last modified 2019-12-4; size 603 bytes
  MD5 checksum 47bd6066637c077873b14b73e810b1e2
  Compiled from "TestDemo.java"
public class com.jesus.util.TestDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #15            // main
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/jesus/util/TestDemo
   #6 = String             #27            // success
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               SUCCESS
   #9 = Utf8               Ljava/lang/String;
  #10 = Utf8               ConstantValue
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               printText
  #18 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #19 = Utf8               SourceFile
  #20 = Utf8               TestDemo.java
  #21 = NameAndType        #11:#12        // "<init>":()V
  #22 = Class              #29            // java/lang/System
  #23 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #24 = Class              #32            // java/io/PrintStream
  #25 = NameAndType        #33:#34        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/jesus/util/TestDemo
  #27 = Utf8               success
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (Ljava/lang/String;)V
{
  public static final java.lang.String SUCCESS;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String success

  public com.jesus.util.TestDemo();
    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 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String main
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8
}
SourceFile: "TestDemo.java"

以上Constant pool中的常量有34個,+1正好就等於constant_pool_count;

2.2.4、常量池

常量池是class文件中佔用空間最大的數據項目之一,由於常量池中的常量數量不固定,所以在常量池前面需要一個u2類型的數據,代表其大小,Java虛擬機指令不依賴於類、接口、類實例或數組的運行時佈局,而是依賴常量池表中的符號信息,常量池的表結構如下(常量池中的每一項常量都是一個表):

cp_info {
  u1 tag; // 標誌位,用於區分常量類型
  u1 info[];
}

常量池表中,開始都是由一個u1類型的tag標誌位開始,代表這個常量的類型,後面的info[]數組內容有tag的值決定,tag值對應關係如下:

        類型                              標誌              描述
CONSTANT_utf8_info                         1          UTF-8編碼的字符串
CONSTANT_Integer_info                      3          整形字面量
CONSTANT_Float_info                        4          浮點型字面量
CONSTANT_Long_info                         5          長整型字面量
CONSTANT_Double_info                       6          雙精度浮點型字面量
CONSTANT_Class_info                        7          類或接口的符號引用
CONSTANT_String_info                       8          字符串類型字面量
CONSTANT_Fieldref_info                     9          字段的符號引用
CONSTANT_Methodref_info                   10          類中方法的符號引用
CONSTANT_InterfaceMethodref_info          11          接口中方法的符號引用
CONSTANT_NameAndType_info                 12          字段或方法的符號引用
CONSTANT_MothodType_info                  16          標誌方法類型
CONSTANT_MethodHandle_info                15          表示方法句柄
CONSTANT_InvokeDynamic_info               18          表示一個動態方法調用點
2.2.4.1、CONSTANT_Methodref_info

如上面class文件中,接下來的1個字節tag標誌位爲0x0a,換算成10進製爲10,由上表可知類型爲CONSTANT_Methodref_info,即類中方法的符號引用,查詢可知他的結構爲:

CONSTANT_Methodref_info {
	u1 tag; // 標誌位,值爲10
	u2 class_index; // 該方法所屬的類在常量池裏的索引
	u2 name_and_type_index; // 該方法的名稱和類型的索引
}

其中class_index的值必須是常量池中的有效索引,並且索引的項必須爲CONSTANT_Class_info結構,表示一個類或者接口,當前方法是這個類或接口的成員,如上面class文件中的0x0007,換算成10進製爲7,即常量池索引爲7,查看上面常量池索引爲7的類型,正好是一個Class類型;

name_and_type_index的值也必須是常量池中的有效索引,並且索引的項必須爲CONSTANT_NameAndType_info結構,表示當前方式的名字和描述符,上面class文件中接下來的2個字節爲0x0015,十進制數爲21,索引21的類型爲NameAndType;

此時看常量池中的第一個常量Methodref類型,後面指向#7,#21,正好對應上面的分析;

2.2.4.2、CONSTANT_Fieldref_info

接下來繼續取1個字節的u1類型,0x09,十進制爲9,對應類型爲CONSTANT_Fieldref_info,即字段索引,字段索引的結構與CONSTANT_Methodref_info結構一樣,後面繼續取2個字節的u2類型的class_index,0x0016,十進制爲22,也對應Class類型,後面2個字節0x0017,十進制爲23,取常量池索引爲23的即NameAndType類型,常量池中他指向#22,#23;

2.2.4.3、CONSTANT_String_info

繼續取1個字節的u1標誌位,0x08,即CONSTANT_String_info類型,結構爲:

CONSTANT_String_info {
	u1 tag; // 標誌位,值爲8
	u2 string_index; // 指向字符串字面量的索引
}

string_index的值必須爲常量池中的有效索引,其指向的成員必須是CONSTANT_utf8_info結構,表示Unicode碼點序列,最終會被初始化成爲一個String對象;

繼續取2個字節的u2類型,0x000f,即十進制15,常量池中的索引爲15的類型爲Utf8,而第3個常量後面也是指向#15;

略過後面的0x0a 0018 0019,類型爲CONSTANT_Methodref_info;

2.2.4.4、CONSTANT_Class_info

繼續取後面1個字節,0x07,即7對應CONSTANT_Class_info類型,其結構爲:

CONSTANT_Class_info {
	u1 tag;  // 標誌位,值爲7
	u2 name_index; // 全限定名常量的索引值
}

name_index值必須爲常量池中的有效索引,其成員必須是CONSTANT_utf8_info結構,表示一個有效的類或接口二進制名稱的內部形式;

取後面2個字節0x001a,即十進制26,指向常量池中的#26,即Utf8類型;

略過後面0x08 001b 07 001c

2.2.4.5、CONSTANT_utf8_info

繼續取2個字節標誌位0x01,對應類型爲CONSTANT_utf8_info,其結構爲:

CONSTANT_utf8_info {
	u1 tag; // 標誌位,值爲1
	u2 length; // utf-8編碼的字符串佔用的字節數
	u1 bytes[length]; // 長度爲length的utf-8編碼的字符串
}

length值指明瞭bytes[]數組的長度,其確定CONSTANT_utf8_info結構中的內容長度;

bytes[]表示字符串值的byte數組;

如上class文件,繼續取2個字節的length長度值0x0007,即十進制7,表示長度爲7,即後面繼續取7個u1類型作爲常量值,即0x5355 4343 4553 53,對比ASCII碼值表,該常量爲“SUCCESS”,即在java文件中聲明的常量值;

略過後面的0x01 0012 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b,“Ljava/lang/String”;

略過後面的0x01 000d 43 6f6e 7374 616e 7456 616c 7565,“ConstantValue”;

略過後面的0x01 0006 3c 696e 6974 3e, “”;

0x01 0003 2829 56,“()V”;

…後面幾個Utf8略過;

此處,由於class文件中方法、字段等都需要引用CONSTANT_Utf8_info類型常量來描述名稱,所以CONSTANT_Utf8_info類型常量的最大長度就是length的最大值,即u2類型能表達的最大值,0xffff,對應十進制數爲65535,也就是Java中方法或變量的最大數量不能超過64KB,即65535;

2.2.4.6、CONSTANT_NameAndType_info

繼續取後面的0x0c,對應類型爲CONSTANT_NameAndType_info,其結構爲:

CONSTANT_NameAndType_info {
	u1 tag; // 標誌位,值爲12
	u2 name_index; // 該字段或方法名稱常量的索引
	u2 descriptor_index; // 該字段或方法描述符常量的索引
}

其中name_index的值必須是常量池中的有效索引,索引處的成員必須是CONSTANT_utf8_info結構,該結構要麼表示特殊的方法名,要麼表示一個有效的字段或方法的非限定名,如上面class文件中,0x000b,即十進制11,對應常量池中#11,Utf8類型;

descriptor_index的值必須是常量池中的有效索引,索引處的成員必須是CONSTANT_utf8_info結構,表示一個有效的字段描述符或方法描述符,如上面class文件中的0x000c,即十進制12,對應常量池中#12,即Utf8類型;

略過後面0x07 001d;

…後面幾個NameAndType、Class、Utf8略過,直到將常量池中的所有常量都解析完畢;

2.2.4.7、CONSTANT_Integer_innfo
{
	u1 tag; // 標誌位,值爲3
	u4 bytes; // 按照高位在淺存儲的int值
}
2.2.4.8、CONSTANT_Float_info
{
	u1 tag; // 標誌位,值爲4
	u4 bytes; // 按照高位在淺存儲的float值
}
2.2.4.9、CONSTANT_Long_info
{
	u1 tag; // 標誌位,值爲5
	u8 bytes; // 按照高位在淺存儲的long值
}
2.2.4.10、CONSTANT_Double_info
{
	u1 tag; // 標誌位,值爲6
	u8 bytes; // 按照高位在淺存儲的double值
}
2.2.4.11、CONSTANT_InterfaceMethodref_info
{
	u1 tag; // 標誌位,值爲11
	u2 index; // 聲明方法的接口描述符CONSTANT_Class_info的索引項
	u2 index; // 指向名稱及類型描述符CONSTANT_NameAndType的索引
}
2.2.4.12、CONSTANT__MethodHandle_info
{
	u1 tag; // 標誌位,值爲15
	u1 reference_kind; // 值爲1~9,決定了方法句柄類型
	u2 reference_index; // 必須是常量池的有效索引
}
2.2.4.13、CONSTANT_MethodType_info
{
	u1 tag; // 標誌位,值爲16
	u2 descriptor_index; // 必須是常量池的有效索引,類型必須是CONSTANT_Utf8_info結構,表示方法描述符
}
2.2.4.14、CONSTANT_InvokeDynamic_info
{
	u1 tag; // 標誌位,值爲18
	u2 bootstrap_method_attr_index; // 值必須是當前Class文件中引導方法表的bootstrap_methods[]數組的有效索引
	u2 namee_and_type_index; // 必須是常量池的有效索引,結構必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述
}

2.2.5、訪問標誌

access_flags爲訪問標誌,用於表示某個類或者接口的訪問權限及屬性,是類還是接口等,取值對應如下:

  標誌名                值          含義
ACC_PUBLIC           0x0001     聲明爲public,可以包外訪問
ACC_FINAL            0x0010     聲明爲final,不允許有子類
ACC_SUPER            0x0020     當用到invokespecial指令時,需要對父類方法做特殊處理
ACC_INTERFACE        0x0200     該class文件定義的是接口而不是類
ACC_ABSTRACT         0x0400     聲明爲abstract,不能被實例化
ACC_SYNTHETIC        0x1000     聲明爲synthetic,表示該class文件並非由Java源代碼所生成
ACC_ANNOTATION       0x2000     標識註解類型
ACC_ENUM             0x4000     標識枚舉類型

取後面2個字節u2類型,即0x0021,發現並未找到對應的值,其實該值爲所有訪問標誌通過或運算得來的值,0x0021=0x0001|0x0020,即public和super,通過javap命令打印出的類標誌符也爲:flags: ACC_PUBLIC, ACC_SUPER,正好對應;

對於Java SE 8及後續版本,無論class文件中的這個標誌的實際值是什麼,也不管class文件的版本號是多少,Java虛擬機都認爲每個class文件均設置了ACC_SUPER標誌;

2.2.6、類索引

類索引,u2類型,this_class的值必須是常量池中某個有效索引,並且索引處的成員必須爲CONSTANT_Class_info結構,表示class文件所定義的類或者接口,如上面class文件,繼續取後面2個字節的u2類型,0x0005,對應常量池中的#5,即Class類型;

2.2.7、父類索引

父類索引,u2類型,super_class值要麼是0,要麼是對常量池中某項的一個有效索引,如果不爲0,在常量池中的引用處成員必須是CONSTANT_Class_info類型,表示這class文件所定義的類的直接超類,如果爲0,那麼這個class只可能用來表示Object類,因爲他是唯一沒有父類的類;

取上面class文件的2個字節u2類型,0x0007,十進制7,常量池中#7,即Class類型;

2.2.8、接口計數器

接口計數器,u2類型,interfaces_count的值表示接口索引表的容量,即當前類或者接口的直接超接口數量,取上面class文件的2個字節u2類型,0x0000,即0,沒有接口,該值爲0時,接口表不佔用任何字節;

2.2.9、接口表

接口表,interfaces[]中每個成員的值必須是常量池中的某個有效索引,長度爲interfaces_count,每個成員必須是CONSTANT_Class_info結構,由於接口數量爲0,所以這裏沒有接口;

2.2.10、字段計數器

字段計數器,u2類型,fields_count表示當前class文件的字段數量,如上class文件,取2個字節u2類型,0x0001,即該類有1個成員字段,即java文件中定義了一個String類型的成員字段SUCCESS;

2.2.11、字段表

字段表fields[fields_count],用於描述接口或類中聲明的變量,其中每個成員的結構均爲field_info,用於描述當前類或者接口中某個字段的完整描述,只描述當前類或接口,不包括父類或父接口集成的字段,結構如下:

field_info {
	u2              access_flags;
	u2              name_index;
	u2              descriptor_index;
	u2              attributes_count;
	attributes_info attributes[attributes_count];
}

其中access_flags表示子彈的訪問權限和基本屬性,對應的含義如下:

   標誌名            值            說明
ACC_PUBLIC        0x0001       聲明爲public,可以包外訪問
ACC_PRIVATE       0x0002       聲明爲private,只能在定義該字段的類訪問
ACC_PROTECTED     0x0004       聲明爲protected,子類可以訪問
ACC_STATIC        0x0008       聲明爲static
ACC_FINAL         0x0010       聲明爲final
ACC_VOLATILE      0x0040       聲明爲volatile,被表識的字段無法緩存  
ACC_TRANSIENT     0x0080       聲明爲transient,被標識的字段不會爲持久化對象管理器所寫入或讀取
ACC_SYNTHETIC     0x1000       被標識的字段由編譯器產生,而沒有寫源代碼中
ACC_ENUM          0x4000       枚舉的成員

如上class文件中,取字段數量後面的u2類型的access_flags,即0x0019,即0x0019=0x0001|0x0008|0x0010,對應PUBLIC,STATIC,FINAL,正對應java文件的SUCCESS字段的訪問限定符”public static final“;

name_index的值必須爲常量池中的一個有效索引,該處的成員必須是CONSTANT_Utf8_info結構,表示一個有效的非限定名,這個名詞對應於本字段,取2個字節的u2類型,即0x0008,對應常量池中的#8,即Utf8類型,對應值爲”SUCCESS“,即字段名;

descriptor_index的值必須爲常量池的一個有效索引,類型必須是CONSTANT_Utf8_info,表示一個有效的字段描述符,用來描述字段的數據類型、方法的參數列表(包括數量、類型及順序)和返回值,取2個字節的u2類型,0x0009,即對應常量池的#9,Utf8類型,返回值用一個大寫字符來表示,如下:

標識字符        含義
   B        基本類型byte
   C        基本類型char
   D        基本類型double
   F        基本類型float
   I        基本類型int
   J        基本類型long
   S        基本類型short
   Z        基本類型boolean
   V        特殊類型void
   L        對象類型,如Ljava/lang/Object

另外,對於數組類型,每一位度將使用一個前置”[“字符來描述,例如定義一個String[][]的二維數組,將被表示爲”[[Ljava/lang/String;“,整型數組爲”[I“;

方法描述的順序爲:先參數列表後返回值,參數列表按照參數的嚴格順序放在一組小括號”()“內,例如:

int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述爲:”([CII{CIII)I“;

接着取後面u2類型的2個字節0x0009,十進制爲9,常量池中#9爲Utf8,值爲”Ljava/lang/String;“,即該變量爲String類型;

attributes_count的值表示當前字段的附加屬性的數量,取2個字節的u2類型,0x0001,即有一個附加屬性;

attributes[]屬性表中的每個成員,一個字段可以關聯任意多個屬性(屬性見下文2.2.15);

取2個字節的attribute_name_index,0x000a,十進制爲10,對應常量池有效索引#10,對應Utf8類型,即”ConstantValue“(屬性見後文2.2.15.1);

ConstantValue的結構爲(後面分析):

{
	u2 attribute_name_index; // 標識
	u4 attribute_length; // 固定爲2
	u2 constantvalue_index; // 常量池中的一個有效索引
}

根據ConstantValue的結構,attribute_length固定爲2,取u4類型的4個字節,0x0000 0002(見後文2.2.15.1);

繼續取u2類型的2個字節ConstantValue的constantvalue_index,即0x0006,對應常量池#6,即String類型,值爲”success“;

2.2.12、方法計數器

方法計數器的結構爲u2類型,即表示該類或接口中定義了多少個方法;

如上class文件中,取u2類型的2個字節0x0003,即對應該類中定了三個方法,觀察java文件對應的方法爲2個,類中未顯示定義無參構造方法時,編譯器會默認加上,所以爲3個方法;

2.2.13、方法表

方法表中的方法描述和字段描述幾乎一樣,同上,方法吧的結構爲:

{
	u2             access_flags; // 訪問限定符
	u2             name_index; // 名稱索引
	u2             descripotr_index; // 描述符索引
	u2             attributes_count; // 屬性數量
	attribute_info attributes; // 屬性表集合
}

方法表訪問標誌符如下表:

  標誌名稱             標誌值          含義
ACC_PUBLIC           0x0001     方法是否爲public
ACC_PRIVATE          0x0002     方法是否爲private
ACC_PROTECTED        0x0004     方法是否爲protected
ACC_STATIC           0x0008     方法是否爲static
ACC_FINAL            0x0010     方法是否爲final
ACC_SYNCHRONIZED     0x0020     方法是否爲synchronized
ACC_BRIDGE           0x0040     方法是否由編譯器產生的橋接方法
ACC_VARARGS          0x0080     方法是否接受不定參數
ACC_NATIVE           0x0100     方法是否爲native
ACC_ABSTRACT         0x0400     方法是否爲abstract
ACC_STRICTFP         0x0800     方法是否爲strictfp
ACC_SYNTHETIC        0x1000     方法是否是由編譯器自動產生的

同字段表描述一樣,取u2類型2個字節0x0001的access_flags,即對應PUBLIC;

u2類型的2個字節的name_index,0x000b,十進制爲11,常量池中#11爲Utf8類型,對應值爲,即實例構造器;

u2類型的2個字節的descripotr_index,0x000c,十進制爲12,常量池中#12爲Utf8,值爲”()V“,即無參的返回值爲void;

u2類型的2個字節的attributes_count,0x0001,即一個屬性值;

取u2類型的2個字節的屬性標識,0x000d,十進制爲13,常量池中#13爲Utf8,值爲”Code“,該值爲固定值(見下文屬性2.2.15.2);

u4類型4個字節attribute_length,0x0000 001d,對應十進制29,屬性值的長度爲29,往後29個字節都是該方法的描述即0x0001 0001 0000 0005 2a b7 00 01 b1 0000 0001 000e 0000 0006 0001 0000 0003;

u2類型max_stacks,0x0001,操作數棧深度爲1,u2類型max_locals,0x0001,局部變量空間爲1(對應javap生成的內容可驗證);

u4類型字節碼長度,0x0000 0005,即字節碼長度爲5;

連續取5個u1類型的字節碼,0x2a、0xb7、0x00、0x01、0xb1,查詢虛擬機字節碼指令表可知分別對應:aload_0、invokespecial、return;

字節碼指令詳見《深入理解Java虛擬機·附錄B 虛擬機字節碼指令表》

  • aload_0:將第一個引用類型本地變量推送至棧頂;
  • invokespecial:調用超類構造方法,實例初始化方法,私有方法,該方法有一個u2類型的參數說明,即後面0x0001,結構爲CONSTANT_Utf8_info,常量池中#1值爲”java/lang/Object.""😦)V“,由此可知爲無參的構造方法;
  • return:從當前方法返回void,執行這條指令後,當前方法結束;

由上分析可對應javap生成的內容驗證,對應3條指令:

Code:
	stack=1, locals=1, args_size=1
		0: aload_0
		1: invokespecial #1                  // Method java/lang/Object."<init>":()V
		4: return

備註:上面locals=1,args_size=1,但是方法沒有參數,在非static方法中,默認有一個this參數;

備註:字節碼指令解析時,需要注意的是有些字節碼指令是帶參數的,如上面的invokespecial,後面跟着兩個參數;

接着後面是u2類型的異常數量以及異常表,0x0000,即該方法沒有異常表;

繼續取u2類型的attribute_count,0x0001,即1個屬性;

取u2類型2個字節屬性名,0x000e,十進制爲14,常量池中#14爲Utf8,值爲:”LineNumberTable“(見後文2.2.15.3);

根據下文中的LineNumberTable結構,取u4類型的4個字節的attribute_length,0x0000 0006,即屬性值的長度爲6,繼續取u2類型的2個字節的line_number_table_length,0x0001,即line_number_table集合長度爲1,line_number_table結構爲2個u2類型,分別對應start_pc和line_number,即0x0000、0x0003,分別對應字節碼指令行號爲0,Java源碼行號爲3,與javap生成的內容對比驗證一致:

LineNumberTable:
	line 3: 0

後面其他方法的分析,main方法:

0x0009 000f 0010 0001 000d 0000 0025 0002 0001 0000 0009 b200 0212 03b6 0004 b100 0000 0100 0e00 0000 0a00 0200 0000 0800 0800 09

printText方法:

0x0002 0011 0012 0001 000d 0000 0026 0002 0002 0000 000a b200 022b b600 0412 06b0 0000 0001 000e 0000 000a 0002 0000 000c 0007 000d

2.2.14、屬性計數器

屬性計數器,u2類型,表明該類的屬性數量,例如本class文件的0x0001,即1個屬性描述;

2.2.15、屬性表

Class文件、字段表、方法表都可以攜帶自己的屬性表集合,用於描述某些場景專有的信息;

例如上面class文件中,分析到最後,字節碼文件只剩下”0x0013 0000 0002 0014“,根據屬性表的結構,取u2類型的2個字節的屬性名,0x0013,十進制爲19,常量池中#19爲Utf8,值爲”SourceFile“(下文2.2.15.4),接着看後面的SourceFile的結構分析,取u4類型的4個字節的屬性長度,0x0000 0002,即兩個長度,取u2類型的2個字節屬性描述,0x0014,十進制爲20,常量池中#20爲Utf8,值爲”TestDemo.java“;

通過驗證javap生成的文件可知正好對應:

SourceFile: "TestDemo.java"

屬性表的關鍵字常用部分如下(詳見《深入理解Java虛擬機》):

屬性名稱              使用位置              含義
Code                 方發表          Java代碼編譯成的字節碼指令
ConstantValue        字段表          final關鍵字定義的常量值
InnerClasses         類文件          內部類列表
LineNumberTable     Code屬性        Java源碼的行號與字節碼指令的對應關係
localVariableTable  Code屬性        方法的局部變量描述
...

對於每個屬性,名稱即標誌位表示的,需要從常量池中引用一個CONSTANT_Utf8_info類型的常量表示,屬性值的結構則由每個不同的屬性自己獨有的結構,需要一個u4長度的屬性去說明屬性值所佔用的位數即可;

屬性表的結構:

{
	u2 attribute_name_index; // 屬性名
	u4 attribute_length; // 屬性值的長度
	u1 info; // 屬性
}
2.2.15.1、ConstantValue

只有被static關鍵字修飾的變量才使用這個屬性,在虛擬機中,對非static類型的變量的賦值是在實例構造器方法中,而對於類變量(static修飾),則在類構造器方法或者使用ConstantValue屬性;

目前Sun Javac編譯器是如果同時使用final和static來修飾,並且這個數據類型爲基本類型或者java.lang.String的話,就生成ConstantValue屬性來進行初始化,如果這個變量沒有被final修飾,或者並非基本類型及字符串,則會選擇在方法中進行初始化;

ConstantValue的結構如下:

{
	u2 attribute_name_index; // 標識
	u4 attribute_length; // 固定爲2
	u2 constantvalue_index; // 常量池中的一個有效索引
}
2.2.15.2、Code

Java程序方法體中的代碼經過Javac編譯後,最終變爲字節碼指令存儲在Code屬性內,保存在方法表中,並非所有方法表都必須有這個屬性,如接口或者抽象類中的方法就不存在Code屬性,Code屬性的結構如下:

{
	u2             attribute_name_index; // 指向CONSTANT_Utf8_info的常量索引,固定爲”Code“,表示屬性名稱
	u4             attribute_length; // 該屬性值的長度,由於屬性名稱索引與屬性長度一共爲6個字節,所以屬性值的長度固定爲整個屬性表長度減去6個字節
	u2             max_stack; // 操作數棧(Operand Stacks)深度的最大值,根據該值來分配棧幀中的操作站深度
	u2             max_locals; // 局部變量表所需的存儲空間,單位爲Slot(不超過32位佔用1個Slot,64位佔2個)
	u4             code_length; // 字節碼長度
	u1             code; // 存儲java源程序編譯後的字節碼指令
	u2             exception_table_length; //
	exception_info exception_table; //
	u2             attribute_count; //
	attribute_info attributes; //
}
2.2.15.3、LineNumberTable

用於描述Java源代碼行號與字節碼行號之間的對應關係,並不是運行時必須的屬性,但是默認會生成到class文件中,有這個信息在程序出現異常時,可以再堆棧中顯示出出錯的行號,結構如下:

{
	u2 arrtibute_name_index; // 屬性名
	u4 attribute_length; // 屬性長度
	u2 line_number_table_length; // line_number_table的長度
	line_number_info line_number_table; // 包括start_pc和line_number兩個u2類型的數據項,前者是字節碼行號,後者是Java源碼行號
}
2.2.15.4、SourceFile

該屬性用於記錄生成這個class文件的源碼文件名稱,在Java中,大多數類名和文件名是一致的,內部類會有一些特殊,有高屬性時,程序拋出異常時,會在堆棧中顯示出代碼所屬的文件名,該屬性爲一個定長屬性,結構如下:

{
	u2 attribute_name_index; // 屬性名
	u4 attribute_length; // 屬性長度
	u2 sourcefile_index; // 文件名索引
}

其餘屬性值具體見《深入理解Java虛擬機》,書中有詳細介紹;

參考書籍
《深入理解Java虛擬機》
《Java虛擬機規範》

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