1. 程式人生 > 實用技巧 >還搞不懂類檔案結構?跟我一起擼一遍class就都明白了

還搞不懂類檔案結構?跟我一起擼一遍class就都明白了

二刷《深入理解Java虛擬機器》的時候,發現看類檔案結構這章依舊雲裡霧裡。因為類檔案中的結構實在太多了,尤其在涉及表的時候,經常會出現表中巢狀表的情況。
有協議學習經驗的同學一定知道最快了解一種協議的方法就是參照規則自己將協議解析一次。眾所周知類檔案中存的也是位元組(所以class檔案也叫位元組碼檔案),針對類檔案結構的解析就像是對某種協議的解析。因此,可以將《深入理解Java虛擬機器》第6章中的內容作為工具書來對照(因為書中提供了各種結構的說明和解析規則),自己寫一個類,針對編譯後產生的class檔案解析一遍就能對類檔案結構有個大致的印象了。

類檔案結構:

在開始解析類檔案前,需要先對class檔案的大致結構做一個初步瞭解,後續的解析也將會根據這個結構分成幾部分來解析。

  • 校驗資訊:
    • 魔數:4位元組(固定值0xcafe babe)
    • 次版本號:2位元組
    • 主版本號:2位元組,由十進位制的45開始。
  • 常量池:
    • 常量池計數(constant_pool_count):2位元組,表示常量池中常量的個數,其中下標為0的常量並不會出現在常量池中,也就是說常量池中的索引實際上是從1開始的,之所以要將下標為0的常量預留是為了滿足常量池中某一常量不引用任何常量的情況(Java語言中是否有這種情況還需要確認)。
    • 常量池(constant_pool):每個常量的資料結構並不是固定的,由常量的型別決定,但是每個常量的第一個位元組決定了常量的型別。
  • 類資訊:
    • 訪問標誌(access_flags
      ):2位元組,表示類和介面的訪問控制
    • 類資訊(this_class):2位元組
    • 父類資訊(super_class):2位元組
    • 介面資訊,包括介面個數(interfaces_count, 2位元組)和介面資訊(interfaces 一個interface是2位元組,總共interfaces_counts * 2個位元組)
  • 欄位資訊:
    • 欄位個數(fields_count): 2位元組,表示之後有幾個field_info的結構
    • 欄位表(field_info):欄位表,是一個表結構,長度不固定,結構稍複雜
  • 方法資訊:
    • 方法個數(methods_count):2位元組,表示之後有幾個(method_info
      )方法表結構
    • 方法表(methods_info):方法表,長度不固定,可能是最複雜的一個結構了。
  • 屬性表資訊:
    • 屬性表個數(attributes_count):2位元組,表示之後有幾個屬性表結構
    • 屬性表(attribute_info)屬性表資訊,表結構,長度不固定。屬性表資訊可能會同樣巢狀在欄位表,方法表的表結構中。

對Class檔案解析前的準備

對Class檔案的解析首先需要寫一個類用作測試,然後編譯該類生成class檔案,在針對該class檔案進行解析。
為了更好的對比解析過程的正確性,我們可以通過javap -p -verbose命令先對class檔案反編譯,輸出結果。

Java程式碼:

package com.insanexs.mess.javap;

public class JavapTest {

    protected static final String VAR_CONSTANT = "CONSTANT";

    private volatile int intField;

    private int[] intArraysField;

    private String strField;

    public JavapTest(){

    }

    public void publicMethod(){

    }

    protected String protectedReturnStrMethod(){
        return strField;
    }

    private synchronized void privateSynchronizedMethod(int intArgs){
        intField = intArgs;
    }
}

編譯生成的class檔案如下(已經按各部分拆開):

開始分析

校驗資訊

魔數

魔數是class檔案的前四個位元組,固定為0xcafe babe。用途是判斷檔案是否正確。

主次版本號

0x0000 0034 其中前兩個位元組表示次版本號,後兩個位元組表示主版本號。0x34轉成十進位制為52,52-45+1 = 8(第一個主版本號從45開始),因此推斷出Java版本為JDK 8。主次版本號用來校驗位元組碼和JVM是否匹配,JVM的版本需要大於等於class檔案的版本號。

常量池

常量池中存量了兩種型別的常量:字面量和符號引用。
字面量可以理解為常量的值(無法修改的值)。
符號引用則包括三種類型:類或介面的全限定名,欄位的名稱和描述符,方法的名稱和描述符(後兩種情況其實是很對稱的,一個針對欄位另一個針對方法)。

常量池計數constant_pool_count

校驗資訊後,接下來的兩個位元組表示常量池中常量的個數,這裡是0x0024,轉成十進位制為36。說明常量池中共有36個常量。
但是由於下標為0的常量是由JVM故意空出的,不會顯示出現在位元組碼中,因此實際常量池的常量從1開始,直到35。

常量池

我們已經知道了常量池共有36個常量(實際只有35個),但由於不同型別的常量結構並不是固定的,我們無法通過常量個數直接推出之後多少位元組屬於常量池的內容。
因此,我們只能逐個解析常量池中的常量,直到解析出常量的個數達到constant_pool_count - 1,常量池才算解析完成。
由於篇幅限制,本文不會解析全部常量池,只會解析其中一些常量用作示範,讀者可根據示範自行完成剩下的常量解析。
上面已經說了不同型別的常量其結構也是不同的,但是所有常量的第一個位元組都是標誌位,因此解析常量池中的常量的方式是解析一個位元組,根據該位元組確定常量的型別,在查詢對應的結構完成剩餘部分的解析。

譬如,這裡的第一個常量#1:

第一個位元組:0x0a,對比常量池的型別後發現是一個CONSTANT_Methodref_info(類中方法的符號引用)。
在查詢該型別的結構,應該是:

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

應該是一個位元組的標識位(tag),2個位元組的類名稱索引(class_index)和2個位元組的名稱和型別描述符(name_and_type_index),共5個位元組。
因此常量池#1對應的位元組分別是:0a 0005 001e(含我們已經分析過的0a)。其中class_index 為0x0005,name_and_type_index為0x001e。這兩個index的值均是指向常量池的其他常量的,轉成十進位制分別是指向#5和#30。
這樣就解析完了常量池中的第一個常量。

//常量#1
0a //tag 表示型別為Methodref_info(類中方法) 其結構為(U1 flag;U2 index(指向類描述符); U2 index(指向名稱和型別描述符)) 
0005 //指向常量池中0x05的常量 => #5
001e //指向常量池中0x1e的常量 => #30
同理我們解析常量池中的第二個常量#2:

首先,第一個位元組是0x09,查表確定型別為CONSTANT_Fieldref_info(欄位的符號引用)。確定其結構和CONSTANT_Methodref_info相同:

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

同樣解析得:

//常量#2
09 //0x09表示型別為Fieldref_info(類中欄位) 其結構為(U1 flag; U2 index(指向類描述符); U2 index(指向名稱和型別描述))=>
0004 //指向常量池中0x04的常量 =>#4
001f //指向常量池中0x1f的常量 =>#31

之後的解析過程不再贅述,直接貼上解析常量池#3-#35的結果:

//常量#3
09 //同樣是Fieldref_info
0004 // =>#4
0020 // =>#32
//常量#4
07 //0x07表示Class_info(類或介面的符號引用) 其結構為(U1 flag; U2 index(指向全限定名常量))
0021 //指向常量池中的0x21 => #33
//常量#5
07 //同樣是Class_info型別
0022 // => #34
//常量#6
01 //0x01表示Utf8_info 表示一個UTF8字串常量(U1 flag; U2 length(字串佔的位元組數); U1 數量為length個,表示byte)說明理論上JVM的字串常量的位元組上線為65535???
000c //length = 0x0c 表示之後12個位元組是字串常量位元組內容
5641 525f 434f 4e53 5441 4e54 //UTF-8字串的內容 用工具翻譯成字串表示為:VAR_CONSTANT
//常量#7
01 //同樣是Utf8_info
0012 //length = 18
4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b //翻譯成字串為:Ljava/lang/String;
//常量#8
01 //同樣是Utf8_info
000d //length = 13
436f 6e73 7461 6e74 5661 6c75 65 //翻譯成字串為:ConstantValue
//常量#9
08 //0x08表示String_info 表示字串字面常量(U1 flag; U2 index(指向字串字面量))
0023 //指向常量池中的0x23 =>#35
//常量#10
01 //Utf8_info
0008 //length = 8
696e 7446 6965 6c64 //翻譯成字串:intField
//常量#11
01 //Utf8_info
0001 //length = 1
49 //翻譯成字串:I
//常量#12
01 //Utf8_info
000e //length = 14
696e 7441 7272 6179 7346 6965 6c64 //翻譯成字串為:intArraysField
//常量#13
01 //Utf8_info
0002 //length = 2
5b49 //翻譯成字串為:[I
//常量#14
01 //Utf8_info
0008 //length = 8
7374 7246 6965 6c64 //翻譯成字串為:strField
//常量#15
01 //Utf8_info
0006 //length = 6
3c69 6e69 743e //翻譯成字串為:<init>
//常量#16
01 //Utf8_info
0003 //length = 3
2829 56 //翻譯成字串為:()V
//常量#17
01 //Utf8_info
0004 // length = 4
436f 6465 //翻譯成字串為:Code
//常量#18
01 //Utf8_info
000f //length = 15
4c69 6e65 4e75 6d62 6572 5461 626c 65 //翻譯成字串為:LineNumberTable
//常量#19
01 //Utf8_info
0012 //length = 18
4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65 //翻譯成字串為:LocalVariableTable
/常量#20
01 //Utf8_info
0004 //length = 4
7468 6973 //翻譯成字串為:this
//常量#21
01 //Utf8_info
0023 //length = 35
4c 636f 6d2f 696e 7361 6e65 7873 2f6d 6573 732f 6a61 7661 702f 4a61 7661 7054 6573 743b //翻譯成字串為:Lcom/insanexs/mess/javap/JavapTest;
//常量#22
01 //Utf8_info
000c //length = 12
70 7562 6c69 634d 6574 686f 64 //翻譯成字串為:publicMethod
//常量#23
01 //Utf8_info
0018 //length = 24
7072 6f74 6563 7465 6452 6574 7572 6e53 7472 4d65 7468 6f64 //翻譯成字串為:protectedReturnStrMethod
//常量#24
01 //Utf8_info
0014 //length = 20
28 294c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b //翻譯成字串為:()Ljava/lang/String;
//常量#25
01 //Utf8_info
0019 //length = 25
7072 6976 6174 6553 796e 6368 726f 6e69 7a65 644d 6574 686f 64 //翻譯成字串為:privateSynchronizedMethod
//常量#26
01 //Utf8_info
0004 //length = 4
2849 2956 //翻譯成字串為:(I)V
//常量#27
01 //Utf8_info
0007 //length = 7
69 6e74 4172 6773 //翻譯成字串為:intArgs
//常量#28
01 //Utf8_info
000a //length = 10
53 6f75 7263 6546 696c 65 //翻譯成字串為:SourceFile
//常量#29
01 //Utf8_info
000e //length = 14
4a61 7661 7054 6573 742e 6a61 7661 //翻譯成字串為:JavapTest.java
//常量#30
0c //0x0c表示NameAndType_info 表示欄位或方法的部分符號引用(U1 flag; U2 index(指向欄位或方法的名稱常量); U2 index(指向欄位或方法的描述符常量))
000f //指向常量池中的0x0f => #15
0010 //指向常量池中的0x10 => #16
//常量#31
0c //同樣是NameAndType_info
000e //指向常量池中的0x0e => #14
0007 //指向常量池中的0x07 => #7
//常量#32
0c //同樣是NameAndType_info
000a //指向常量池中的0x0a => #10
000b //指向常量池中的0x0b => #11
//常量#33
01 //Utf8_info
0021 //length = 33
636f 6d2f 696e 7361 6e65 7873 2f6d 6573 732f 6a61 7661 702f 4a61 7661 7054 6573 74 //翻譯成字串為:com/insanexs/mess/javap/JavapTest
//常量#34
01 //Utf8_info
0010 //length = 16
6a61 7661 2f6c 616e 672f 4f62 6a65 6374 //翻譯成字串為:java/lang/Object
//常量#35
01 //Utf8_info
0008 //length = 8
43 4f4e 5354 414e 54 //翻譯成字串為:CONSTANT

上述常量池得很多常量都直接(或是比較直接的)出現在我們得程式碼中,比如欄位名稱,類名稱,方法全限定名等。有一部分是根據程式碼能推測,比如方法描述符等,但是還有一部分似乎不明所以,比如上述的CODE,LineNumberTable等。
不著急,這些常量在之後得解析中會再次遇到。

類資訊

在解析完常量池的資料後,接下來的一部分資料表示類得一些資訊。從上述得位元組碼得0x0021開始(29行的最後一組)
類資訊這部分主要有類的訪問控制屬性,類索引和父類索引(指向常量池中的常量,通常是類得全限定名),介面個數和介面索引(介面索引同樣指向常量池中的常量)。

訪問控制access_flag

訪問控制佔兩個位元組(16位,每一個二進位制代表一種標誌,因此理論上最多能有16種標誌,Java SE 8中定義了8種)。上述檔案中對應訪問控制標誌的位元組位0x0021。0x0021 = (0x0020 | 0x0001),查表得出ACC_SUPER,ACC_PUBLIC這個類的訪問控制標誌位。 現代版本編譯的類都會帶有ACC_SUPER,而ACC_PUBLIC表示這個類是public的。

類索引this_class

類索引佔兩個位元組,同樣指向常量池中的常量(型別為類的符號飲用)。這裡對應的資料為:

0004 //this_class U2,指向常量池中的Class_info 這裡指向常量池#4

父類索引super_class

父類索引和類索引類似,同樣佔兩個位元組,指向常量池中的常量,只不過指向的類的符號飲用程式碼的是父類,這裡對應的資料為:

0005 //super_class U2 同樣指向常量池中的Class_info 這裡指向常量池#5

介面個數interfaces_count和介面索引interface

之後的兩個位元組表示類實現的介面的個數,然後對應的interfaces_count * 2個位元組表示介面的索引資料。由於我們的測試類沒有實現任何介面,因此interfaces_counts為0,之後也沒有表示介面資料的位元組。

0000 //interface_count 表示介面的個數 這裡為0 表示類沒有實現介面,之後也沒有位元組表示介面索引

欄位資訊

分析完類資訊後,之後的資料表示類中欄位的資訊。這裡分為兩部分:欄位個數field_count和欄位表field_info

欄位個數field_count

欄位個數表示後面將會有幾個欄位表結構,因為欄位表結構長度也不是固定的,因此也只能解析完所有的欄位表後才能繼續解析下一部分內容,無法直接通過欄位個數推出之後的多少位元組表示欄位資訊相關的資料。
欄位個數佔兩個位元組。

0004 //field_count 表示欄位的個數 這裡為4 表示接下來的4個欄位表結構

欄位表field_info

欄位表介面較為複雜,因為其是一個表介面,且可能巢狀其他表。
先來看一下欄位表的介面:

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

access_flags佔兩位元組,表示欄位的訪問屬性,name_index佔兩位元組,指向常量池中的常量,表示欄位名稱,descriptor_index佔兩位元組,同樣指向常量池中的常量,表示欄位的描述符,attributes_count佔兩位元組,表示後面有幾個屬性表,attribute_info即屬性表,用來描述額外屬性,為表結構,長度不固定。

前文已知我們的測試類會有4個欄位表,我們這裡只針對第一個欄位表分析,後續的讀者可以自己按規則解析。
首先分析固定的前八個位元組:

001c //access_flags 欄位的訪問屬性 0x1c = (0x10 | 0x08 | 0x04 ) =>ACC_PROTECTED ACC_FINAL ACC_STATIC
0006 //name_index 指向常量池中#6 即變數名為VAR_CONSTANT
0007 //descriptor_index 指向常量池#7 即描述符Ljava/lang/String; 說明是String型別的欄位
0001 //attributes_count 表示有1個attribute_info

如果某個欄位代表的attributes_count的位元組值為0,那麼對這個欄位的解析就已經完成了,但是好巧不巧的,這裡分析的attributes_count為1,說明之後還有一些位元組是用來表示屬性表的。
屬性表用來描述某些特定的額外資訊,其整個結構並非是固定長度的,甚至可能屬性表中巢狀屬性表的情況。
瞭解下屬性表的通用結構:

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

attribute_name_index:2位元組,指向常量池中的常量,表示常量的名稱(Java SE 8規定能識別的常量型別有23種)。
attribute_length:4位元組,表示之後還有多少長度的位元組均資料該屬性表的內容
info:不固定長位元組,解析方式由屬性表的型別決定。
可以看到前六個位元組都是相同的,然後由attribute_length表示之後還有多少個位元組,這一點和一些變長協議類似。
回到我們需要解析的資料中來,屬於欄位1中屬性表的位元組應該是0008 0000 0002 0009
通過解析前兩個位元組0008,我們得知其指向常量池中的#8,為即ConstantValue。這是當欄位被final修飾後,出現在欄位中的屬性表,表示一個常量。該屬性表的結構如下:

ConstantValue_attribute {
 u2 attribute_name_index;
 u4 attribute_length;
 u2 constantvalue_index;
}

可以看到其代表attribute_length的四位元組資料為0000 0002(因為constantvalue_index的長度固定為2),對比我們的位元組資料也確實如此。然後我們解析代表constantvalue_index的位元組0009,表示指向常量池中#9,為CONSTANT。正好是常量的值。

之後還剩三個欄位,有興趣的讀者可以自己分析,這裡直接貼上結果:

//field1
001c //access_flags 欄位的訪問屬性 0x1c = (0x10 | 0x08 | 0x04 ) =>ACC_PROTECTED ACC_FINAL ACC_STATIC
0006 //name_index 指向常量池中0x06 => #6 即VAR_CONSTANT
0007 //descriptor_index 指向常量池中0x07 => #7 即Ljava/lang/String; 說明是String型別的欄位
0001 //attributes_count 表示有1個attribute_info 屬性表 attribute_info是一個比較複雜的結構,虛擬機器規範中定義了虛擬機器應當識別的二十多種屬性(Java SE 8 23種)所有屬性的開始的6位元組都是相同的(U2 attribute_name_index + U4 attribute_length),之後的結構由屬性自己定義,屬性表可以出現在類,欄位及方法上
0008 //attribute_name_index 指向常量池中的Utf8_info常量 0x08 => #8 即ConstantValue ConstantValue是屬性表的一種,出現在欄位中,表示final定義的常量值
0000 0002 //length = 2 表示後面2個位元組長度的資料為該屬性表的資料
0009 //對於ConstantValue而言 這部分資料表示constantvalue_index 指向常量池中的常量 0x09即 =>#9 即String_info 具體值為#35 為字串 CONSTANT

//filed2
0042 //access_flags =>(0x40 | 0x02) => ACC_PRIVATE ACC_VOLATILE
000a //name_index 指向常量池中的0x0a #10 即intField
000b //descriptor_index 指向常量池中的0x0b #11 即I 表示int型別的field
0000 //attribute_count = 0 說明無attribute_info

//filed3
0002 //access_flags => 0x02 =>ACC_PRIVATE
000c //name_index 指向常量池中的0x0c #12 即intArraysField
000d //descriptor_index 指向常量池中的0x0d #13 即[I 表示int陣列
0000 //attribute_count = 0 說明無attribute_info

//field4
0002 //access_flags => 0x02 =>ACC_PRIVATE
000e //name_index 指向常量池中的0x0e #14 即strField
0007 //descriptor_index 指向常量池中的0x07 #7 即 Ljava/lang/String; 說明是String型別的欄位
0000 //attribute_count = 0 說明無attribute_info

方法資訊

解析完欄位資訊,之後的位元組是從0x0004開始(32行最後一個位元組和33行第一個位元組),方法資訊和欄位資訊的解析其實很對成,同樣顯示通過methods_count表示之後有幾個方法表,再逐個解析方法表method_info,直到達到方法個數。方法表的整體結構和欄位表的整體結構也是類似的,只是方法表上的屬性表會更多,因此,解析起來要比欄位複雜。

方法個數methods_count

methods_count佔兩個位元組,表示之後共有多少個方法表。0004表示之後有四個方法表。

方法表method_info

方法表和欄位表結構是對稱的,通用結構如下:

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

可以看到其結構和field_info是相同的,只不過name_index指向常量池中表示的方法名稱的常量,descriptor_index則指向常量池中表示方法描述符的常量。
我們以第一個方法表為例,先解析前八個固定的位元組。
access_flag0001,對應標識位為ACC_PUBLIC,表示為共有方法。
name_index對應的位元組為000f,指向常量池#15,即方法名為:
descriptor_index0010,指向常量池的#16,即方法描述符為:()V 無參無返回。
attributes_count0001,表示之後有一個attribute_info屬性表。
屬性表的解析方式在解析欄位過程時,已經介紹過了。
首先是根據前兩位元組確認屬性表的型別:0011,指向常量池中#17,發現是我們之前不明所以的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];
}

可以看到CODE屬性表是一個非固定長度的結構。

先看相對簡單的部分:

attribute_name_index:屬性表名稱索引,2位元組,表示屬性表的型別。
attribute_length:屬性長度,4位元組,表示之後多少個位元組的內容均屬於該屬性表。對於CODE這種非固定長度的屬性表結構而言,長度顯得格外重要。
max_stack:2位元組,表示運算元棧的最大深度(注意和方法的呼叫棧深度是不同的概念)。
max_locals:2位元組,區域性變量表的大小,單位是slot,一個slot可以存放32位長度的資料,但是像long和double型別的變數,需要使用兩個slot。
code_length:位元組碼長度,表示之後n個位元組均和JVM位元組碼指令相關。
code:位元組碼,JVM位元組碼指令佔一個位元組。但是部分位元組碼指令需要帶上引數(因此消耗了部分位元組資料)。

參考例子,對這部分結果的解析如下:

0011 //attribute_name_index 指向常量池中的0x11 #17 即Code 表示CODE屬性表 CODE屬性表結構為(U2 attribute_name_index + U4 attribute_length + U2 max_stack + U2 max_locals + U4 code_length + code_length * U1 code +  U2 exception_table_length + exception_table_length * exception_info + U2 attribute_count + attribute_count * attribute_info)
0000 0033 //length = 0x33 表示之後51個位元組為CODE屬性表中的資訊
0001 //max_stack 1
0001 //max_locals 1
0000 0005 //code_length 5 後面接CODE 每個指令佔一個位元組 部分指令後的位元組表示指令的引數
2a b7 00 01 b1 //aload_0 invokespecial (0001 => 常量池#1) return

需要注意的是指令部分b7後面兩個位元組0001是b7(invokespecial)需要的引數,指向常量池#1。因此並非CODE中的每一個位元組都是指令集中的指令。

對異常表的解析

我們這裡的例子並沒有涉及異常處理的程式碼,因此exception_table_length對應的位元組是0000,長度為0,說明之後沒有異常表。
讀者如果感興趣可以自己寫程式碼測試,分析方法類似。

CODE中巢狀屬性表的解析

方法表結構之所以複雜,正是因為經常出現表巢狀表的情況。CODE屬性表就可能含有其他屬性表。
同理,我們解析之後表示attributes_count的兩個位元組——0002。一看還不少,我激動的嚥了咽口水,解析不怕多,就是幹!

第一個屬性表的型別索引0012,表示指向常量池#18,為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];
}

得,結構中前面幾個還好,後面又嵌套了一個比較複雜的屬性。但是沒在怕。
attribute_name_index:這個現在已經很面熟了,表示屬性表的名稱索引。
attribute_length:這個也是老夥計,後面多少位元組依舊是該屬性表得內容。
line_number_table_length:4位元組,表示程式碼行號表的個數,之後有幾個line_number_table
line_number_table:程式碼行號表,一個複雜結構,但是好在是固定的,佔4位元組,前兩位元組start_pc表示一個偏移量,應該是對著之前分析的CODE屬性表中的code部分順序(要注意區分這裡的兩個CODE,大寫的CODE表示屬性表的一種型別,小寫的code表示CODE屬性表中表示位元組碼指令的部分)。後兩個位元組line_number表示指令對應出現在程式碼中的行號。

0012 //attribute_name_index 指向常量池中的0x12 即#18 LineNumberTable 表示行號和位元組碼指令的對應關係
0000 000a //attribute_length 表示後面10個位元組均為該屬性表的資訊
0002 //line_number_table_length 表示後面有2個line_number_table 一個line_number_table結構為(U2 start_pc + U2 line_number)
0000 //start_pc start_pc表示上述指令集中的索引 0對應上述指令集既為2a 為aload_0指令
000d //line_number 等於行號line:13 表示aload_0 對應程式碼13行
0004 //start_pc 同理對應索引為4的指令 return
000f //line_number 等於行號line:15 表示return 對應程式碼15行

在解析第二個屬性表:attribute_name_index代表的位元組是0013,對應常量池#19,為LocalVariableTable 表示方法區域性變數的描述。
其結構如下:

LocalVariableTable_attribute {
 u2 attribute_name_index;
 u4 attribute_length;
 u2 local_variable_table_length;
 { u2 start_pc;
 u2 length;
 u2 name_index;
 u2 descriptor_index;
 u2 index;
 } local_variable_table[local_variable_table_length];
}

前兩個部分已經很熟悉了不再贅述。
local_variable_table_length2位元組,表示區域性變量表的個數。
local_variable_table是一個複雜結構,但也是固定長度的,共10位元組。
start_pc佔2位元組,表示偏移量,length佔2位元組,表示長度,這兩部分資訊結合起來可以確認變數的作用域是從start_pcstart_pc+length
name_index表示2位元組,變數的名稱索引,指向常量池中的常量。descriptor_index表示2位元組,變數的描述符索引,指向常量池中的常量。這兩部分資訊結合起來可以確定變數的名稱和型別。
index表示變數在區域性變量表中的索引,前文已經介紹過了變數儲存在區域性變數中是以slot為單位,這裡的index就表示該變數存放在第幾個slot。

理論分析了,結合情況實踐一下:

0013 //attribute_name_index 指向常量池中的0x13 即#19  LocalVariableTable 表示方法區域性變數的描述
0000 000c //attribute_length 表示之後12個位元組均為 LocalVariableTable 屬性表中的內容
0001 //local_variable_table_length 表示有一個區域性變量表 local_variable_table的結構為(U2 start_pc + U2 length + U2 name_index + U2 descriptor_index + U2 index)
0000 //start_pc 0
0005 //length 5 說明該區域性變數從偏移量0開始到0+5 一直被使用
0014 //name_index 指向常量池中的常量 0x14 => #20 即this
0015 //descriptor_index 指向常量池中的常量 0x15 => #21 即Lcom/insanexs/mess/javap/JavapTest;
0000 //index 0

補充說明一點:類中的非靜態方法 虛擬機器會預設將this指標作為方法的第一個變數。

這樣我們第一個方法就解析完成了,讀者感興趣的可以針對剩下的三個方法實操一下,這裡就直接貼上解析結果:

//method_1
0001 //access_flag => 0x01 => ACC_PUBLIC
000f //name_index 指向常量池中的0x0f #15 即方法名為:<init>
0010 //descriptor_index 指向常量池中的0x10 #16 即方法描述符為:()V 無參無返回
0001 //attributes_count 表示之後有一個attribute_info
0011 //attribute_name_index 指向常量池中的0x11 #17 即Code 表示CODE屬性表 CODE屬性表結構為(U2 attribute_name_index + U4 attribute_length + U2 max_stack + U2 max_locals + U4 code_length + code_length * U1 code +  U2 exception_table_length + exception_table_length * exception_info + U2 attribute_count + attribute_count * attribute_info)
0000 0033 //length = 0x33 表示之後51個位元組為CODE屬性表中的資訊
0001 //max_stack 1
0001 //max_locals 1
0000 0005 //code_length 5 後面接CODE 每個指令佔一個位元組 部分指令後的位元組表示指令的引數
2a b7 00 01 b1 //aload_0 invokespecial (0001 => 常量池#1) return
0000 //exception_table_length = 0 說明沒有異常表的資料 如果exception_table_length為n 後面的n個位元組為異常表相關的資訊
0002 //attributes_count = 2
0012 //attribute_name_index 指向常量池中的0x12 即#18 LineNumberTable 表示行號和位元組碼指令的對應關係
0000 000a //attribute_length 表示後面10個位元組均為該屬性表的資訊
0002 //line_number_table_length 表示後面有2個line_number_table 一個line_number_table結構為(U2 start_pc + U2 line_number)
0000 //start_pc start_pc表示上述指令集中的索引 0對應上述指令集既為2a 為aload_0指令
000d //line_number 等於行號line:13 表示aload_0 對應程式碼13行
0004 //start_pc 同理對應索引為4的指令 return
000f //line_number 等於行號line:15 表示return 對應程式碼15行

0013 //attribute_name_index 指向常量池中的0x13 即#19  LocalVariableTable 表示方法區域性變數的描述
0000 000c //attribute_length 表示之後12個位元組均為 LocalVariableTable 屬性表中的內容
0001 //local_variable_table_length 表示有一個區域性變量表 local_variable_table的結構為(U2 start_pc + U2 length + U2 name_index + U2 descriptor_index + U2 index)
0000 //start_pc 0
0005 //length 5 說明該區域性變數從偏移量0開始到0+5 一直被使用
0014 //name_index 指向常量池中的常量 0x14 => #20 即this
0015 //descriptor_index 指向常量池中的常量 0x15 => #21 即Lcom/insanexs/mess/javap/JavapTest;
0000 //index 0

//method_2
0001 //access_flag =>0x01 =>ACC_PUBLIC
0016 //name_index 指向常量池中的0x16 =>#22 即  publicMethod
0010 //descriptor_index 指向常量池中的0x10 =>#16 即 ()V 表示無參且無返回值
0001 //attribute_count 表示之後有1個attributes_info
0011 //attribute_name_index 同樣指向常量中的0x11 #17即CODE屬性表
0000 002b //length = 43 表示之後43個位元組為CODE屬性表的內容
0000 //max_stack = 0
0001 //max_locals = 1
0000 0001 //code_length = 1
b1 //指令 表示return
0000 //exception_table_length = 0 無異常表
0002 //attributes_count = 2
0012 //attribute_name_index 指向常量池中的#18 LineNumberTable
0000 0006 //attribute_length 表示後6個位元組為LineNumberTable的資訊
0001 //表示只有一個line_number_table
0000 //start_pc 0 對應指令return
0013 //line_number 19  表示return對應的行號是19
0013 //attribute_name_index 指向常量池中的#19 LocalVariableTable
0000 000c //attribute_length 表示之後12個位元組均為 LocalVariableTable 屬性表中的內容
0001 //local_variable_table_length 表示有1個區域性變量表
0000 //start_pc 0
0001 //length 1
0014 //name_index 指向常量池中的常量 0x14 => #20 即this
0015 //descriptor_index 指向常量池中的常量 0x15 => #21 即Lcom/insanexs/mess/javap/JavapTest;
0000 //index 0

//method 3
0004 //access_flag =>0x04 =>ACC_PROTECTED
0017 //name_index 常量池中#23 即 protectedReturnStrMethod
0018 //descriptor_index 常量池中#24 ()Ljava/lang/String; 表示無參,單接返回值型別為String
0001 //attribute_count 表示有一個attribute_info
0011 //attribute_name_index 同樣指向常量中的0x11 #17即CODE屬性表
0000 002f //length = 47 之後47個位元組均為CODE屬性表的內容
0001 //max_stack = 1
0001 //max_locals = 1
0000 0005 //code_length = 5 表示方法含有五個指令
2a b4 00 02 b0 //位元組碼指令 分別表示aload_0 getfield (0002 =>常量池#2) areturn
0000 //exception_table_length = 0 表示無異常表
0002 //attributes_count表示有兩個屬性表
0012 //attribute_name_index 常量池#18 LineNumberTable
0000 0006 //attribute_length 表示後6個位元組為LineNumberTable的資訊
0001 //表示只有一個line_number_table
0000 //start_pc 0 對應的指令aload_0
0016 //line_number 對應line:22
0013 //attribute_name_index 常量池#19 LocalVariableTable
0000 000c //attribute_length 表示之後12個位元組均為 LocalVariableTable 屬性表中的內容
0001 //local_variable_table_length 表示有1個區域性變量表
0000 //start_pc 0
0005 //length 5
0014 //name_index 指向常量池#20 即this
0015 //descriptor_index 指向常量池#21 即Lcom/insanexs/mess/javap/JavapTest;
0000 //index 0

//method 4
0022 //access_flag => (0x20 | 0x02) => ACC_SYNCHRONIZED ACC_PRIVATE
0019 //name_index 常量池中#25 即 privateSynchronizedMethod
001a // descriptor_index 常量池中#26 (I)V 接受一個int引數 但無返回值
0001 //attribute_count 表示有一個attribute_info
0011 //attribute_name_index 同樣指向常量中的0x11 #17即CODE屬性表
0000 003e //length = 62 之後的62個位元組均為CODE屬性
0002 //max_stack = 2
0002 //max_locals = 2
0000 0006 //code_length = 6 表示之後6個位元組均為位元組碼指令
2a 1b b5 00 03 b1 //分別為aload_0 iload_1 putfield (0003 =>常量池#3) return
0000 //exception_table_length = 0 表示無異常表
0002 //attributes_count表示有兩個屬性表
0012 //attribute_name_index 常量池#18 LineNumberTable
0000 000a //attribute_length 表示後10個位元組為LineNumberTable的資訊
0002 //表示有2個line_number_table
0000 //start_pc 0 對應的指令aload_0
001a //line_number 對應line:26
0005 //start_pc 5 對應的指令return
001b //line_number 對應line:27
0013 //attribute_name_index 常量池#19 LocalVariableTable
0000 0016 //attribute_length = 22 之後22個位元組均為區域性變量表的內容
0002 //local_variable_table_length = 2 表示存在兩個區域性變量表
0000 //start_pc 0
0006 //length 6
0014 //name_index 常量池#20 即this
0015 //descriptor_index 指向常量池#21 即Lcom/insanexs/mess/javap/JavapTest;
0000 //index 0
0000 //start_pc 0
0006 //length 6
001b //name_index 指向常量池#27 即intArgs
000b //descriptor_index 指向常量池#11 即I 表示int型別
0001 //index 1

屬性表資訊

在解析完方法後,剩下還有一小部分內容是屬性表資訊。現在我們對屬性表的解析過程可以說是輕車熟路了。
未解析的位元組並不多了:

0001 001c 0000 0002 001d

首先attributes_count0001,說明只有一個屬性表,so easy!
attributes_name_index001c,指向常量池#28,即SourceFile 用於記錄原始檔名稱。
SourceFile的結構如下:

SourceFile_attribute {
 u2 attribute_name_index;
 u4 attribute_length;
 u2 sourcefile_index;
}

定長的結構,最後2個位元組表示原始檔名稱索引,指向常量池。這裡為001d,指向常量池中#29,為JavapTest.java。

這樣,我們就完成了所有的位元組碼檔案解析。完成的解析結果和原始碼已經放在了我的github上。

其他問題的小測試

這裡針對類檔案結構學習過程中,幾個疑問做了下測試。

同步方法和同步塊在類檔案結構中的表示有什麼不同?

上文我們已經測了同步方法,其是通過方法的access_flag的標誌位(ACC_SYNCHRONIZED)表示的。但是這種方式對於同步塊而言已經是不行的,那麼同步塊是如何實現同步控制的呢?
測試程式碼類如下:

public class SynchronizedTest {

    public synchronized void synchronizedMethod(){
        return;
    }

    public static synchronized void staticSynchronizedMethod(){
        return;
    }

    public void synchronizedCode(){
        synchronized (this){
            return;
        }
    }

    public void staticSynchronizedCode(){
        synchronized (SynchronizedTest.class){
            return;
        }
    }
}

使用javap -verbose -c 反編譯class檔案。
擷取相關四個方法,如下:

  public synchronized void synchronizedMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/insanexs/mess/javap/SynchronizedTest;

  public static synchronized void staticSynchronizedMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 15: 0

  public void synchronizedCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: return
         7: astore_2
         8: aload_1
         9: monitorexit
        10: aload_2
        11: athrow
      Exception table:
         from    to  target type
             4     6     7   any
             7    10     7   any
      LineNumberTable:
        line 19: 0
        line 20: 4
        line 21: 7
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lcom/insanexs/mess/javap/SynchronizedTest;
      StackMapTable: number_of_entries = 1
        frame_type = 255 /* full_frame */
          offset_delta = 7
          locals = [ class com/insanexs/mess/javap/SynchronizedTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]

  public void staticSynchronizedCode();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/insanexs/mess/javap/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_1
         6: monitorexit
         7: return
         8: astore_2
         9: aload_1
        10: monitorexit
        11: aload_2
        12: athrow
      Exception table:
         from    to  target type
             5     7     8   any
             8    11     8   any
      LineNumberTable:
        line 25: 0
        line 26: 5
        line 27: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   Lcom/insanexs/mess/javap/SynchronizedTest;
      StackMapTable: number_of_entries = 1
        frame_type = 255 /* full_frame */
          offset_delta = 8
          locals = [ class com/insanexs/mess/javap/SynchronizedTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]

從上面可以分析出:無論是類還是例項的同步方法,都是通過在ACCESS_FLAG中將ACC_SYNCHRONIZED位標識為真實現的。
而對於同步塊,則是通過位元組碼指令中新增monitorenter和monitorexit實現的。而鎖的物件則是根據運算元棧當前的物件所決定。

測試max_stack

這個測試主要是加深對運算元棧的理解,設計一個程式碼,讓其在類檔案結構中的max_stack為2。
滿足上面要求的測試程式碼如下:

public class MaxStackTest {

    public int maxStack2Method(){
        int var1 = 1;
        int var2 = 2;
        return var1 + var2;
    }
}

為什麼說此時運算元棧最大深度為2,因為首先var1從區域性變量表中載入到運算元棧,此時運算元棧的深度為1,接著繼續從區域性變量表中將var2載入到運算元棧,此時棧深度為2。而後為了計算和,運算元棧彈出var2和var1,深度重回0。所以最大深度為2。
同樣可以拿javap命令反編譯驗證:

 public int maxStack2Method();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: ireturn
      LineNumberTable:
        line 11: 0
        line 12: 2
        line 13: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lcom/insanexs/mess/javap/MaxStackTest;
            2       6     1  var1   I
            4       4     2  var2   I
}

測試64位虛擬機器下,int,long,reference分別佔幾個slot

因為在32位虛擬機器下int和reference只佔一個slot,而long和double佔兩個slot,那麼這種情況在64位的虛擬機器下是否依舊如此?
測試程式碼如下:

public class SlotTest {

    private Object reference;

    public void testSlot(){
        int i =0;
        long l = 1L;
        Object reference = new Object();

        int j = 1;
        System.out.println(i + ","+ l + "," + reference + "," + j);
    }
}

通過javap檢視區域性變量表,發現除了long是佔2個slot的,其餘的像int和reference都只佔1個slot。說明和32位的情況一致。
仔細想想,確實如此。因此在class檔案的開頭校驗部分只針對版本號進行了校驗,並不區分是32位還是64位,說明二者的編譯規則應該是一致的。