JVM詳解之:java class文件的密碼本

簡介

一切的一切都是從javac開始的。從那一刻開始,java文件就從我們肉眼可分辨的文本文件,變成了冷冰冰的二進位文件。

變成了二進位文件是不是意味著我們無法再深入的去了解java class文件了呢?答案是否定的。

機器可以讀,人為什麼不能讀?只要我們掌握java class文件的密碼錶,我們可以把二進位轉成十六進位,將十六進位和我們的密碼錶進行對比,就可以輕鬆的解密了。

下面,讓我們開始這個激動人心的過程吧。

一個簡單的class

為了深入理解java class的含義,我們首先需要定義一個class類:

public class JavaClassUsage {

    private int age=18;

    public void inc(int number){
        this.age=this.age+ number;
    }
}

很簡單的類,我想不會有比它更簡單的類了。

在上面的類中,我們定義了一個age欄位和一個inc的方法。

接下來我們使用javac來進行編譯。

IDEA有沒有?直接打開編譯後的class文件,你會看到什麼?

沒錯,是反編譯過來的java程式碼。但是這次我們需要深入了解的是class文件,於是我們可以選擇 view->Show Bytecode:

當然,還是少不了最質樸的javap命令:

 javap -verbose JavaClassUsage

對比會發現,其實javap展示的更清晰一些,我們暫時選用javap的結果。

編譯的class文件有點長,我一度有點不想都列出來,但是又一想只有對才能講述得更清楚,還是貼在下面:

public class com.flydean.JavaClassUsage
  minor version: 0
  major version: 58
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/flydean/JavaClassUsage.age:I
   #8 = Class              #10            // com/flydean/JavaClassUsage
   #9 = NameAndType        #11:#12        // age:I
  #10 = Utf8               com/flydean/JavaClassUsage
  #11 = Utf8               age
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/flydean/JavaClassUsage;
  #18 = Utf8               inc
  #19 = Utf8               (I)V
  #20 = Utf8               number
  #21 = Utf8               SourceFile
  #22 = Utf8               JavaClassUsage.java
{
  public com.flydean.JavaClassUsage();
    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: bipush        18
         7: putfield      #7                  // Field age:I
        10: return
      LineNumberTable:
        line 7: 0
        line 9: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/flydean/JavaClassUsage;

  public void inc(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: aload_0
         2: getfield      #7                  // Field age:I
         5: iload_1
         6: iadd
         7: putfield      #7                  // Field age:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/flydean/JavaClassUsage;
            0      11     1 number   I
}
SourceFile: "JavaClassUsage.java"

ClassFile的二進位文件

慢著,上面javap的結果好像並不是二進位文件!

對的,javap是對二進位文件進行了解析,方便程式設計師閱讀。如果你真的想直面最最底層的機器程式碼,就直接用支援16進位的文本編譯器把編譯好的class文件打開吧。

你準備好了嗎?

來吧,展示吧!

上圖左邊是16進位的class文件程式碼,右邊是對16進位文件的適當解析。大家可以隱約的看到一點點熟悉的內容。

是的,沒錯,你會讀機器語言了!

class文件的密碼本

如果你要了解class文件的結構,你需要這個密碼本。

如果你想解析class文件,你需要這個密碼本。

學好這個密碼本,走遍天下都……沒啥用!

下面就是密碼本,也就是classFile的結構。

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];
}

其中u2,u4表示的是無符號的兩個位元組,無符號的4個位元組。

java class文件就是按照上面的格式排列下來的,按照這個格式,我們可以自己實現一個反編譯器(大家有興趣的話,可以自行研究)。

我們對比著上面的二進位文件一個一個的來理解。

magic

首先,class文件的前4個位元組叫做magic word。

看一下十六進位的第一行的前4個位元組:

CA FE BA BE 00 00 00 3A 00 17 0A 00 02 00 03 07 

0xCAFEBABE就是magic word。所有的java class文件都是以這4個位元組開頭的。

來一杯咖啡吧,baby!

多麼有詩意的畫面。

version

這兩個version要連著講,一個是主版本號,一個是次版本號。

00 00 00 3A

對比一下上面的表格,我們的主版本號是3A=58,也就是我們使用的是JDK14版本。

常量池

接下來是常量池。

首先是兩個位元組的constant_pool_count。對比一下,constant_pool_count的值是:

00 17

換算成十進位就是23。也就是說常量池的大小是23-1=22。

這裡有兩點要注意,第一點,常量池數組的index是從1開始到constant_pool_count-1結束。

第二點,常量池數組的第0位是作為一個保留位,表示「不引用任何常量池項目」,為某些特殊的情況下使用。

接下來是不定長度的cp_info:constant_pool[constant_pool_count-1]常量池數組。

常量池數組中存了些什麼東西呢?

字元串常量,類和介面名字,欄位名,和其他一些在class中引用的常量。

具體的constant_pool中存儲的常量類型有下面幾種:

每個常量都是以一個tag開頭的。用來告訴JVM,這個到底是一個什麼常量。

好了,我們對比著來看一下。在constant_pool_count之後,我們再取一部分16進位數據:

上面我們講到了17是常量池的個數,接下來就是常量數組。

0A 00 02 00 03

首先第一個位元組是常量的tag, 0A=10,對比一下上面的表格,10表示的是CONSTANT_Methodref方法引用。

CONSTANT_Methodref又是一個結構體,我們再看一下方法引用的定義:

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

從上面的定義我們可以看出,CONSTANT_Methodref是由三部分組成的,第一部分是一個位元組的tag,也就是上面的0A。

第二部分是2個位元組的class_index,表示的是類在常量池中的index。

第三部分是2個位元組的name_and_type_index,表示的是方法的名字和類型在常量池中的index。

先看class_index,0002=2。

常量池的第一個元素我們已經找到了就是CONSTANT_Methodref,第二個元素就是跟在CONSTANT_Methodref後面的部分,我們看下是什麼:

07 00 04

一樣的解析步驟,07=7,查表,表示的是CONSTANT_Class。

我們再看下CONSTANT_Class的定義:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

可以看到CONSTANT_Class佔用3個位元組,第一個位元組是tag,後面兩個位元組是name在常量池中的索引。

00 04 = 4, 表示name在常量池中的索引是4。

然後我們就這樣一路找下去,就得到了所有常量池中常量的資訊。

這樣找起來,眼睛都花了,有沒有什麼簡單的辦法呢?

當然有,就是上面的javap -version, 我們再回顧一下輸出結果中的常量池部分:

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/flydean/JavaClassUsage.age:I
   #8 = Class              #10            // com/flydean/JavaClassUsage
   #9 = NameAndType        #11:#12        // age:I
  #10 = Utf8               com/flydean/JavaClassUsage
  #11 = Utf8               age
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/flydean/JavaClassUsage;
  #18 = Utf8               inc
  #19 = Utf8               (I)V
  #20 = Utf8               number
  #21 = Utf8               SourceFile
  #22 = Utf8               JavaClassUsage.java

以第一行為例,直接告訴你常量池中第一個index的類型是Methodref,它的classref是index=2,它的NameAndType是index=3。

並且直接在後面展示出了具體的值。

描述符

且慢,在常量池中我好像看到了一些不一樣的東西,這些I,L是什麼東西?

這些叫做欄位描述符:

上圖是他們的各項含義。除了8大基礎類型,還有2個引用類型,分別是對象的實例,和數組。

access_flags

常量池後面就是access_flags:訪問描述符,表示的是這個class或者介面的訪問許可權。

先上密碼錶:

再找一下我們16進位的access_flag:

沒錯,就是00 21。 參照上面的表格,好像沒有21,但是別怕:

21是ACC_PUBLIC和ACC_SUPER的並集。表示它有兩個access許可權。

this_class和super_class

接下來是this class和super class的名字,他們都是對常量池的引用。

00 08 00 02

this class的常量池index=8, super class的常量池index=2。

看一下2和8都代表什麼:

   #2 = Class              #4             // java/lang/Object
   #8 = Class              #10            // com/flydean/JavaClassUsage

沒錯,JavaClassUsage的父類是Object。

大家知道為什麼java只能單繼承了嗎?因為class文件裡面只有一個u2的位置,放不下了!

interfaces_count和interfaces[]

接下來就是介面的數目和介面的具體資訊數組了。

00 00

我們沒有實現任何介面,所以interfaces_count=0,這時候也就沒有interfaces[]了。

fields_count和fields[]

然後是欄位數目和欄位具體的數組資訊。

這裡的欄位包括類變數和實例變數。

每個欄位資訊也是一個結構體:

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

欄位的access_flag跟class的有點不一樣:

這裡我們就不具體對比解釋了,感興趣的小夥伴可以自行體驗。

methods_count和methods[]

接下來是方法資訊。

method結構體:

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

method訪問許可權標記:

attributes_count和attributes[]

attributes被用在ClassFile, field_info, method_info和Code_attribute這些結構體中。

先看下attributes結構體的定義:

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

都有哪些attributes, 這些attributes都用在什麼地方呢?

其中有六個屬性對於Java虛擬機正確解釋類文件至關重要,他們是:
ConstantValue,Code,StackMapTable,BootstrapMethods,NestHost和NestMembers。

九個屬性對於Java虛擬機正確解釋類文件不是至關重要的,但是對於通過Java SE Platform的類庫正確解釋類文件是至關重要的,他們是:

Exceptions,InnerClasses,EnclosingMethod,Synthetic,Signature,SourceFile,LineNumberTable,LocalVariableTable,LocalVariableTypeTable。

其他13個屬性,不是那麼重要,但是包含有關類文件的元數據。

總結

最後留給大家一個問題,java class中常量池的大小constant_pool_count是2個位元組,兩個位元組可以表示2的16次方個常量。很明顯已經夠大了。

但是,萬一我們寫了超過2個位元組大小的常量怎麼辦?歡迎大家留言給我討論。

本文鏈接://www.flydean.com/jvm-class-file-structure/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!