深入理解JVM之Java字節(jié)碼(.class)文件詳解

前言

作為一個(gè)Java開(kāi)發(fā)者,對(duì)技術(shù)的追求而不僅僅停留在會(huì)用API,會(huì)寫(xiě)基本功能上,要想在技術(shù)上有更高的造詣,就需要深入到原理層面去認(rèn)識(shí)代碼運(yùn)行的機(jī)制。因此,本文從class字節(jié)碼文件的結(jié)構(gòu)入手,一步步來(lái)解剖二進(jìn)制字節(jié)碼的內(nèi)部工作原理,這對(duì)深入理解JVM的運(yùn)行機(jī)制大有裨益,同時(shí),對(duì)于想要使用BCEL來(lái)動(dòng)態(tài)改變Class字節(jié)碼指令的工作也很有幫助(示例:JVM Class字節(jié)碼之三-使用BCEL改變類屬性)。

什么是Class文件

Java字節(jié)碼類文件(.class)是Java編譯器編譯Java源文件(.java)產(chǎn)生的“目標(biāo)文件”。它是一種8位字節(jié)的二進(jìn)制流文件, 各個(gè)數(shù)據(jù)項(xiàng)按順序緊密的從前向后排列, 相鄰的項(xiàng)之間沒(méi)有間隙, 這樣可以使得class文件非常緊湊, 體積輕巧, 可以被JVM快速的加載至內(nèi)存, 并且占據(jù)較少的內(nèi)存空間(方便于網(wǎng)絡(luò)的傳輸)。

Java源文件在被Java編譯器編譯之后, 每個(gè)類(或者接口)都單獨(dú)占據(jù)一個(gè)class文件, 并且類中的所有信息都會(huì)在class文件中有相應(yīng)的描述, 由于class文件很靈活, 它甚至比Java源文件有著更強(qiáng)的描述能力。

class文件中的信息是一項(xiàng)一項(xiàng)排列的, 每項(xiàng)數(shù)據(jù)都有它的固定長(zhǎng)度, 有的占一個(gè)字節(jié), 有的占兩個(gè)字節(jié), 還有的占四個(gè)字節(jié)或8個(gè)字節(jié), 數(shù)據(jù)項(xiàng)的不同長(zhǎng)度分別用u1, u2, u4, u8表示, 分別表示一種數(shù)據(jù)項(xiàng)在class文件中占據(jù)一個(gè)字節(jié), 兩個(gè)字節(jié), 4個(gè)字節(jié)和8個(gè)字節(jié)。 可以把u1, u2, u3, u4看做class文件數(shù)據(jù)項(xiàng)的“類型” 。

Class文件的結(jié)構(gòu)

一個(gè)典型的class文件分為:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,F(xiàn)ields,Methods 和Attributes這十個(gè)部分,用一個(gè)數(shù)據(jù)結(jié)構(gòu)可以表示如下:

class_code.PNG-21.1kB

下面對(duì)class文件中的每一項(xiàng)進(jìn)行詳細(xì)的解釋:

1、magic
在class文件開(kāi)頭的四個(gè)字節(jié), 存放著class文件的魔數(shù), 這個(gè)魔數(shù)是class文件的標(biāo)志,他是一個(gè)固定的值: 0XCAFEBABE 。 也就是說(shuō)他是判斷一個(gè)文件是不是class格式的文件的標(biāo)準(zhǔn), 如果開(kāi)頭四個(gè)字節(jié)不是0XCAFEBABE, 那么就說(shuō)明它不是class文件, 不能被JVM識(shí)別。

2、minor_version 和 major_version
緊接著魔數(shù)的四個(gè)字節(jié)是class文件的此版本號(hào)和主版本號(hào)。
隨著Java的發(fā)展, class文件的格式也會(huì)做相應(yīng)的變動(dòng)。 版本號(hào)標(biāo)志著class文件在什么時(shí)候, 加入或改變了哪些特性。 舉例來(lái)說(shuō), 不同版本的javac編譯器編譯的class文件, 版本號(hào)可能不同, 而不同版本的JVM能識(shí)別的class文件的版本號(hào)也可能不同, 一般情況下, 高版本的JVM能識(shí)別低版本的javac編譯器編譯的class文件, 而低版本的JVM不能識(shí)別高版本的javac編譯器編譯的class文件。 如果使用低版本的JVM執(zhí)行高版本的class文件, JVM會(huì)拋出java.lang.UnsupportedClassVersionError 。具體的版本號(hào)變遷這里不再討論, 需要的讀者自行查閱資料。

3、constant_pool
在class文件中, 位于版本號(hào)后面的就是常量池相關(guān)的數(shù)據(jù)項(xiàng)。 常量池是class文件中的一項(xiàng)非常重要的數(shù)據(jù)。 常量池中存放了文字字符串, 常量值, 當(dāng)前類的類名, 字段名, 方法名, 各個(gè)字段和方法的描述符, 對(duì)當(dāng)前類的字段和方法的引用信息, 當(dāng)前類中對(duì)其他類的引用信息等等。 常量池中幾乎包含類中的所有信息的描述, class文件中的很多其他部分都是對(duì)常量池中的數(shù)據(jù)項(xiàng)的引用,比如后面要講到的this_class, super_class, field_info, attribute_info等, 另外字節(jié)碼指令中也存在對(duì)常量池的引用, 這個(gè)對(duì)常量池的引用當(dāng)做字節(jié)碼指令的一個(gè)操作數(shù)。此外,常量池中各個(gè)項(xiàng)也會(huì)相互引用。

常量池是一個(gè)類的結(jié)構(gòu)索引,其它地方對(duì)“對(duì)象”的引用可以通過(guò)索引位置來(lái)代替,我們知道在程序中一個(gè)變量可以不斷地被調(diào)用,要快速獲取這個(gè)變量常用的方法就是通過(guò)索引變量。這種索引我們可以直觀理解為“內(nèi)存地址的虛擬”。我們把它叫靜態(tài)池的意思就是說(shuō)這里維護(hù)著經(jīng)過(guò)編譯“梳理”之后的相對(duì)固定的數(shù)據(jù)索引,它是站在整個(gè)JVM(進(jìn)程)層面的共享池。

class文件中的項(xiàng)constant_pool_count的值為1, 說(shuō)明每個(gè)類都只有一個(gè)常量池。 常量池中的數(shù)據(jù)也是一項(xiàng)一項(xiàng)的, 沒(méi)有間隙的依次排放。常量池中各個(gè)數(shù)據(jù)項(xiàng)通過(guò)索引來(lái)訪問(wèn), 有點(diǎn)類似與數(shù)組, 只不過(guò)常量池中的第一項(xiàng)的索引為1, 而不為0, 如果class文件中的其他地方引用了索引為0的常量池項(xiàng), 就說(shuō)明它不引用任何常量池項(xiàng)。class文件中的每一種數(shù)據(jù)項(xiàng)都有自己的類型, 相同的道理,常量池中的每一種數(shù)據(jù)項(xiàng)也有自己的類型。 常量池中的數(shù)據(jù)項(xiàng)的類型如下表:

constant_pool.PNG-25.4kB

每個(gè)數(shù)據(jù)項(xiàng)叫做一個(gè)XXX_info項(xiàng),比如,一個(gè)常量池中一個(gè)CONSTANT_Utf8類型的項(xiàng),就是一個(gè)CONSTANT_Utf8_info 。除此之外, 每個(gè)info項(xiàng)中都有一個(gè)標(biāo)志值(tag),這個(gè)標(biāo)志值表明了這個(gè)常量池中的info項(xiàng)的類型是什么, 從上面的表格中可以看出,一個(gè)CONSTANT_Utf8_info中的tag值為1,而一個(gè)CONSTANT_Fieldref_info中的tag值為9 。

Java程序是動(dòng)態(tài)鏈接的, 在動(dòng)態(tài)鏈接的實(shí)現(xiàn)中, 常量池扮演者舉足輕重的角色。 除了存放一些字面量之外, 常量池中還存放著以下幾種符號(hào)引用:
(1) 類和接口的全限定名
(2) 字段的名稱和描述符
(3) 方法的名稱和描述符
我們有必要先了解一下class文件中的特殊字符串, 因?yàn)樵诔A砍刂校?特殊字符串大量的出現(xiàn),這些特殊字符串就是上面說(shuō)的全限定名和描述符。對(duì)于常量池中的特殊字符串的了解,可以參考此文檔:Java class文件格式之特殊字符串_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

4、access_flag 保存了當(dāng)前類的訪問(wèn)權(quán)限

5、this_cass 保存了當(dāng)前類的全局限定名在常量池里的索引

6、super class 保存了當(dāng)前類的父類的全局限定名在常量池里的索引

7、interfaces 保存了當(dāng)前類實(shí)現(xiàn)的接口列表,包含兩部分內(nèi)容:interfaces_count 和interfaces[interfaces_count]
interfaces_count 指的是當(dāng)前類實(shí)現(xiàn)的接口數(shù)目
interfaces[] 是包含interfaces_count個(gè)接口的全局限定名的索引的數(shù)組

8、fields 保存了當(dāng)前類的成員列表,包含兩部分的內(nèi)容:fields_count 和 fields[fields_count]
fields_count是類變量和實(shí)例變量的字段的數(shù)量總和。
fileds[]是包含字段詳細(xì)信息的列表。

9、methods 保存了當(dāng)前類的方法列表,包含兩部分的內(nèi)容:methods_count和methods[methods_count]
methods_count是該類或者接口顯示定義的方法的數(shù)量。
method[]是包含方法信息的一個(gè)詳細(xì)列表。

10、attributes 包含了當(dāng)前類的attributes列表,包含兩部分內(nèi)容:attributes_count 和 attributes[attributes_count]
class文件的最后一部分是屬性,它描述了該類或者接口所定義的一些屬性信息。attributes_count指的是attributes列表中包含的attribute_info的數(shù)量。
屬性可以出現(xiàn)在class文件的很多地方,而不只是出現(xiàn)在attributes列表里。如果是attributes表里的屬性,那么它就是對(duì)整個(gè)class文件所對(duì)應(yīng)的類或者接口的描述;如果出現(xiàn)在fileds的某一項(xiàng)里,那么它就是對(duì)該字段額外信息的描述;如果出現(xiàn)在methods的某一項(xiàng)里,那么它就是對(duì)該方法額外信息的描述。

通過(guò)示例代碼來(lái)手動(dòng)分析class文件

上面大致講解了一下class文件的結(jié)構(gòu),這里,我們拿一個(gè)class文件做一個(gè)簡(jiǎn)單的分析,來(lái)驗(yàn)證上面的文件結(jié)構(gòu)是否確實(shí)是如此。

我們?cè)谶@里新建一個(gè)java文件,Hello.java,具體內(nèi)容如下:

    public class Hello{
      private int test;
      public int test(){
            return test;
        }
    }

然后再通過(guò)javac命令將此java文件編譯成class文件:

javac /d/class_file_test/Hello.java

編譯之后的class文件十六進(jìn)制結(jié)果如下所示,可以用UltraEdit等十六進(jìn)制編輯器打開(kāi),得到:

hello_class_test.PNG-22.6kB

接下來(lái)我們就按照class文件的格式來(lái)分析上面的一串?dāng)?shù)字,還是按照之前的順序來(lái):

  1. magic:
    CA FE BA BE ,代表該文件是一個(gè)字節(jié)碼文件,我們平時(shí)區(qū)分文件類型都是通過(guò)后綴名來(lái)區(qū)分的,不過(guò)后綴名是可以隨便修改的,所以僅靠后綴名不能真正區(qū)分一個(gè)文件的類型。區(qū)分文件類型的另個(gè)辦法就是magic數(shù)字,JVM 就是通過(guò) CA FE BA BE 來(lái)判斷該文件是不是class文件

  2. version字段
    00 00 00 34,前兩個(gè)字節(jié)00是minor_version,后兩個(gè)字節(jié)0034是major_version字段,對(duì)應(yīng)的十進(jìn)制值為52,也就是說(shuō)當(dāng)前class文件的主版本號(hào)為52,次版本號(hào)為0。下表是jdk 1.6 以后對(duì)應(yīng)支持的 Class 文件版本號(hào):

    image_1c2th3nii1otd13vg1vhg1hl6itg4i.png-23.7kB
  3. 常量池,constant_pool:
    3.1. constant_pool_count
    緊接著version字段下來(lái)的兩個(gè)字節(jié)是:00 12代表常量池里包含的常量數(shù)目,因?yàn)樽止?jié)碼的常量池是從1開(kāi)始計(jì)數(shù)的,這個(gè)常量池包含17個(gè)(0x0012-1)常量。

    3.2.constant_pool
    接下來(lái)就是分析這17個(gè)常量:

3.2.1. 第一個(gè)變量 0a 00 04 00 0e
首先,緊接著constant_pool_count的第一個(gè)字節(jié)0a(tag=10)根據(jù)上面的表格(文中第二張圖片)

  ![image_1c2tj6ib6pslkbb1876ot81rjj4v.png-4kB][7]

可知,這表示的是一個(gè)CONSTANT_Methodref。CONSTANT_Methodref的結(jié)構(gòu)如下:

               CONSTANT_Methodref_info {
                         u1 tag;    //u1表示占一個(gè)字節(jié)
                         u2 class_index;    //u2表示占兩個(gè)字節(jié)
                         u2 name_and_type_index;    //u2表示占兩個(gè)字節(jié)
               }

其中class_index表示該方法所屬的類在常量池里的索引,name_and_type_index表示該方法的名稱和類型的索引。常量池里的變量的索引從1開(kāi)始。

那么這個(gè)methodref結(jié)構(gòu)的數(shù)據(jù)如下:

                    0a  //tag  10表示這是一個(gè)CONSTANT_Methodref_info結(jié)構(gòu)
                    00 04 //class_index 指向常量池中第4個(gè)常量所表示的類
                    00 0e  //name_and_type_index 指向常量池中第14個(gè)常量所表示的方法

3.2.2. 第二個(gè)變量09 00 03 00 0F
接著是第二個(gè)常量,它的tag是09,根據(jù)上面的表格可知,這表示的是一個(gè)CONSTANT_Fieldref的結(jié)構(gòu),它的結(jié)構(gòu)如下:

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

和上面的變量基本一致。

                        09 //tag
                        00 03 //指向常量池中第3個(gè)常量所表示的類
                        00 0f //指向常量池中第15個(gè)常量所表示的變量

3.2.3. 第三個(gè)變量 07 00 10
tag為07表示是一個(gè)CONSTANT_Class變量,這個(gè)變量的結(jié)構(gòu)如下:

                CONSTANT_Class_info {
                         u1 tag;
                         u2 name_index;
               }

除了tag字段以外,還有一個(gè)name_index的值為00 10,即是指向常量池中第16個(gè)常量所表示的Class名稱。

3.2.4. 第四個(gè)變量07 00 11
同上,也是一個(gè)CONSTANT_Class變量,不過(guò),指向的是第17個(gè)常量所表示的Class名稱。

3.2.5. 第五個(gè)變量 01 00 04 74 65 73 74
tag為1,表示這是一個(gè)CONSTANT_Utf8結(jié)構(gòu),這種結(jié)構(gòu)用UTF-8的一種變體來(lái)表示字符串,結(jié)構(gòu)如下所示:

                    CONSTANT_Utf8_info {
                                   u1 tag;
                                   u2 length;
                                   u1 bytes[length];
                    }

其中l(wèi)ength表示該字符串的字節(jié)數(shù),bytes字段包含該字符串的二進(jìn)制表示。

                        01 //tag  1表示這是一個(gè)CONSTANT_Utf8結(jié)構(gòu)
                        00 04 //表示這個(gè)字符串的長(zhǎng)度是4字節(jié),也就是后面的四個(gè)字節(jié)74 65 73 74
                        74 65 73 74 //通過(guò)ASCII碼表轉(zhuǎn)換后,表示的是字符串“test”

接下來(lái)的8個(gè)變量都是字符串,這里就不具體分析了。

3.2.6. 第十四個(gè)常量 0c 00 07 00 08
tag為0c,表示這是一個(gè)CONSTANT_NameAndType結(jié)構(gòu),這個(gè)結(jié)構(gòu)用來(lái)描述一個(gè)方法或者成員變量。具體結(jié)構(gòu)如下:

                    CONSTANT_NameAndType_info {
                              u1 tag;
                              u2 name_index;
                              u2 descriptor_index;
                    }

name_index表示的是該變量或者方法的名稱,這里的值是0007,表示指向第7個(gè)常量,即是<init>

descriptor_index指向該方法的描述符的引用,這里的值是0008,表示指向第8個(gè)常量,即是()V,由前面描述符的語(yǔ)法可知,這個(gè)方法是一個(gè)無(wú)參的,返回值為void的方法。

綜合兩個(gè)字段,可以推出這個(gè)方法是void <init>()。也即是指向這個(gè)NameAndType結(jié)構(gòu)的Methodref的方法名為void <init>(),也就是說(shuō)第一個(gè)常量表示的是void <init>()方法,這個(gè)方法其實(shí)就是此類的默認(rèn)構(gòu)造方法。

3.2.7. 第十五個(gè)常量也是一個(gè)CONSTANT_NameAndType,表示的方法名為“int test()”,第2個(gè)常量引用了這個(gè)NameAndType,所以第二個(gè)常量表示的是“int test()”方法。

3.2.8. 第16和17個(gè)常量也是字符串,可以按照前面的方法分析。

3.3. 完整的常量池
最后,通過(guò)以上分析,完整的常量池如下:

          00 12  常量池的數(shù)目 18-1=17
          0a 00 04 00 0e  方法:java.lang.Ojbect void <init>()
          09 00 03 00 0f   方法 :Hello int test() 
          07 00 10  字符串:Hello
          07 00 11 字符串:java.lang.Ojbect
          01 00 04 74 65 73 74 字符串:test
          01 00 01 49  字符串:I
          01 00 06 3c 69 6e 69 74 3e 字符串:<init>
          01 00 03 28 29 56 字符串:()V
          01 00 04 43 6f 64 65 字符串:Code 
          01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 字符串:LineNumberTable 
          01 00 03 28 29 49 字符串:()I
          01 00 0a 53 6f 75 72 63 65 46 69 6c 65 字符串:SourceFile
          01 00 0a 48 65 6c 6c 6f 2e 6a 61 76 61 字符串:Hello.java
          0c 00 07 00 08 NameAndType:<init> ()V
          0c 00 05 00 06 NameAndType:test I
          01 00 05 48 65 6c 6c 6f 字符串:Hello
          01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 字符串: java/lang/Object

通過(guò)這樣分析其實(shí)非常的累,我們只是為了了解class文件的原理才來(lái)一步一步分析每一個(gè)二進(jìn)制字節(jié)碼。JDK提供了現(xiàn)成的工具可以直接解析此二進(jìn)制文件,即javap工具(在JDK的bin目錄下),我們通過(guò)javap命令來(lái)解析此class文件:

javap -v -p -s -sysinfo -constants /d/class_file_test/Hello.class

解析得到的結(jié)果為:

class_file_2.PNG-44.1kB

關(guān)于此表每一項(xiàng)的詳細(xì)分析,可以參考國(guó)外的這一篇文檔:JVM Internals
關(guān)于此表中Method操作指令aload_1,getfield,ireturn的作用,可以參考云溪社區(qū)的這篇文章:
JVM Class詳解之二 Method字節(jié)碼指令

發(fā)現(xiàn)了沒(méi)有,上面生成代碼中的Constant pool跟我們上面分析出來(lái)的完整常量池一模一樣,有木有!有木有?
這說(shuō)明我們上面的分析的完成正確!

由此,我們終于弄懂了Constant pool的內(nèi)幕。

接下來(lái)繼續(xù)看其他的字段。

  1. access_flag(u2)
    00 21這兩個(gè)字節(jié)的數(shù)據(jù)表示這個(gè)變量的訪問(wèn)標(biāo)志位,JVM對(duì)訪問(wèn)標(biāo)示符的規(guī)范如下:
access_flag.PNG-26.8kB

這個(gè)表里面無(wú)法直接查詢到0021這個(gè)值,原因是0021=0020+0001,也就是表示當(dāng)前class的access_flag是ACC_PUBLIC|ACC_SUPER。ACC_PUBLIC和代碼里的public 關(guān)鍵字相對(duì)應(yīng)。ACC_SUPER表示當(dāng)用invokespecial指令來(lái)調(diào)用父類的方法時(shí)需要特殊處理。

  1. this_class(u2) 00 03
    this_class指向constant pool的索引值,該值必須是CONSTANT_Class_info類型,這里是3,即指向常量池中的第三項(xiàng),即是“Hello”。

  2. super_class 00 04
    super_class存的是父類的名稱在常量池里的索引,這里指向第四個(gè)常量,即是“java/lang/Object”。

  3. interfaces
    interfaces包含interfaces_count和interfaces[]兩個(gè)字段。因?yàn)檫@里沒(méi)有實(shí)現(xiàn)接口,所以就不存在interfces選項(xiàng),所以這里的interfaces_count為0(0000),所以后面的內(nèi)容也對(duì)應(yīng)為空。

  4. fields

               00 01 fields count        //表示成員變量的個(gè)數(shù),此處為1個(gè)
               00 02 00 05 00 06 00 00   //成員變量的結(jié)構(gòu)

每個(gè)成員變量對(duì)應(yīng)一個(gè)field_info結(jié)構(gòu):

               field_info {
                         u2 access_flags; 0002
                         u2 name_index; 0005
                         u2 descriptor_index; 0006
                         u2 attributes_count; 0000
                         attribute_info attributes[attributes_count];
               }

access_flags為0002,即是ACC_PRIVATE
name_index指向常量池的第五個(gè)常量,為“test”
descriptor_index指向常量池的第6個(gè)常量為“I”
三個(gè)字段結(jié)合起來(lái),說(shuō)明這個(gè)變量是"private int test"。
接下來(lái)的是attribute字段,用來(lái)描述該變量的屬性,因?yàn)檫@個(gè)變量沒(méi)有附加屬性,所以attributes_count為0,attribute_info為空。

  1. methods
    00 02 00 01 00 07 00 08 00 01 00 09 ...
    最前面的2個(gè)字節(jié)是method_count
    method_count:00 02,為什么會(huì)有兩個(gè)方法呢?我們明明只寫(xiě)了一個(gè)方法,這是因?yàn)镴VM 會(huì)自動(dòng)生成一個(gè)<init>方法,這個(gè)是類的默認(rèn)構(gòu)造方法。

接下來(lái)的內(nèi)容是兩個(gè)method_info結(jié)構(gòu):

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

前三個(gè)字段和field_info一樣,可以分析出第一個(gè)方法是“public void <init>()”

                         00 01 ACC_PUBLIC
                         00 07  <init>
                         00 08  V()

接下來(lái)是attribute字段,也即是這個(gè)方法的附加屬性,這里的attributes_count =1,也即是有一個(gè)屬性。
每個(gè)屬性的都是一個(gè)attribute_info結(jié)構(gòu),如下所示:

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

JVM預(yù)定義了部分attribute,但是編譯器自己也可以實(shí)現(xiàn)自己的attribute寫(xiě)入class文件里,供運(yùn)行時(shí)使用。不同的attribute通過(guò)attribute_name_index來(lái)區(qū)分。JVM規(guī)范里對(duì)以下attribute進(jìn)行了預(yù)定義:

21718047_1346754834pJjH.png-65.4kB

這里的attribute_name_index值為0009,表示指向第9個(gè)常量,即是Code。Code Attribute的作用是保存該方法的結(jié)構(gòu)如所對(duì)應(yīng)的字節(jié)碼,具體的結(jié)構(gòu)如下所示:

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

attribute_length表示attribute所包含的字節(jié)數(shù),這里為0000001d,即是39個(gè)字節(jié),不包含attribute_name_index和attribute_length字段。
max_stack表示這個(gè)方法運(yùn)行的任何時(shí)刻所能達(dá)到的操作數(shù)棧的最大深度,這里是0001
max_locals表示方法執(zhí)行期間創(chuàng)建的局部變量的數(shù)目,包含用來(lái)表示傳入的參數(shù)的局部變量,這里是0001.
接下來(lái)的code_length表示該方法的所包含的字節(jié)碼的字節(jié)數(shù)以及具體的指令碼。
這里的字節(jié)碼長(zhǎng)度為00000005,即是后面的5個(gè)字節(jié) 2a b7 00 01 b1為對(duì)應(yīng)的字節(jié)碼指令的指令碼。
參照下表可以將上面的指令碼翻譯成對(duì)應(yīng)的助記符:

               2a   aload_0    
               b7   invokespecial
               00   nop
               01   aconst_null
               b1   return

這即是該方法被調(diào)用時(shí),虛擬機(jī)所執(zhí)行的字節(jié)碼

接下來(lái)是exception_table,這里存放的是處理異常的信息。
每個(gè)exception_table表項(xiàng)由start_pc,end_pc,handler_pc,catch_type組成。start_pc和end_pc表示在code數(shù)組中的從start_pc到end_pc處(包含start_pc,不包含end_pc)的指令拋出的異常會(huì)由這個(gè)表項(xiàng)來(lái)處理;handler_pc表示處理異常的代碼的開(kāi)始處。catch_type表示會(huì)被處理的異常類型,它指向常量池里的一個(gè)異常類。當(dāng)catch_type為0時(shí),表示處理所有的異常,這個(gè)可以用來(lái)實(shí)現(xiàn)finally的功能。

不過(guò),這段代碼里沒(méi)有異常處理,所以exception_table_length為0000,所以我們不做分析。

接下來(lái)是該方法的附加屬性,attributes_count為0001,表示有一個(gè)附加屬性。
attribute_name_index為000a,指向第十個(gè)常量,為L(zhǎng)ineNumberTable。這個(gè)屬性用來(lái)表示code數(shù)組中的字節(jié)碼和java代碼行數(shù)之間的關(guān)系。這個(gè)屬性可以用來(lái)在調(diào)試的時(shí)候定位代碼執(zhí)行的行數(shù)。LineNumberTable的結(jié)構(gòu)如下:

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

前面兩個(gè)字段分別表示這個(gè)attribute的名稱是LineNumberTable以及長(zhǎng)度為00000006。接下來(lái)的0001表示line_number_table_length,表示line_number_table有一個(gè)表項(xiàng),其中start_pc為 00 00,line_number為 00 00,表示第0行代碼從code的第0個(gè)指令碼開(kāi)始。

后面的內(nèi)容是第二個(gè)方法,具體就不再分析了。

  1. attributes
    最后剩下的內(nèi)容是attributes,這里的attributes表示整個(gè)class文件的附加屬性,不過(guò)結(jié)構(gòu)還是和前面的attribute保持一致。00 01表示有一個(gè)attribute。
    Attribute結(jié)構(gòu)如下:
          SourceFile_attribute {
               u2 attribute_name_index;
               u4 attribute_length;
               u2 sourcefile_index;
          }

attribute_name_index為000c,指向第12個(gè)常量,為SourceFile,說(shuō)明這個(gè)屬性是Source
attribute_length為00000002
sourcefile_index為000d,表示指向常量池里的第13個(gè)常量,為Hello.java
這個(gè)屬性表明當(dāng)前的class文件是從Hello.java文件編譯而來(lái)。

字節(jié)碼修改技術(shù)

對(duì)Java Class字節(jié)碼分析,我們應(yīng)該能夠比較清楚的認(rèn)識(shí)到整個(gè)字節(jié)碼的結(jié)構(gòu)。

那通過(guò)了解字節(jié)碼,我們可以做些什么呢?

其實(shí)通過(guò)字節(jié)碼能做很多平時(shí)我們無(wú)法完成的工作。比如,在類加載之前添加某些操作或者直接動(dòng)態(tài)的生成字節(jié)。

ASM 是一個(gè) Java 字節(jié)碼操控框架。它能夠以二進(jìn)制形式修改已有類或者動(dòng)態(tài)生成類。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。不過(guò)ASM在創(chuàng)建class字節(jié)碼的過(guò)程中,操縱的級(jí)別是底層JVM的匯編指令級(jí)別,這要求ASM使用者要對(duì)class組織結(jié)構(gòu)和JVM匯編指令有一定的了解。

目前字節(jié)碼修改技術(shù)有ASM,javassist,cglib,BCEL等。cglib就是基于封裝的Asm. Spring 就是使用cglib代理庫(kù)。關(guān)于cglib的使用介紹,可以參考:CGLIB介紹與原理

Javassist是一個(gè)開(kāi)源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫(kù)。是由東京工業(yè)大學(xué)的數(shù)學(xué)和計(jì)算機(jī)科學(xué)系的 Shigeru Chiba (千葉 滋)所創(chuàng)建的。它已加入了開(kāi)放源代碼JBoss 應(yīng)用服務(wù)器項(xiàng)目,通過(guò)使用Javassist對(duì)字節(jié)碼操作為JBoss實(shí)現(xiàn)動(dòng)態(tài)AOP框架。javassist是jboss的一個(gè)子項(xiàng)目,其主要的優(yōu)點(diǎn),在于簡(jiǎn)單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機(jī)指令,就能動(dòng)態(tài)改變類的結(jié)構(gòu),或者動(dòng)態(tài)生成類。

參考文檔

  1. Java Code to Byte Code
  2. JVM Internals
  3. 從字節(jié)碼層面看“HelloWorld”
  4. ASM官網(wǎng)
  5. CGLIB介紹與原理
  6. CGLIB(Code Generation Library)詳解
  7. JVM之字節(jié)碼——Class文件格式
  8. 云溪社區(qū)--JVM Class詳解之一
  9. 云溪社區(qū)--JVM Class字節(jié)碼之三-使用BCEL改變類屬性
  10. 國(guó)外翻譯文章:Java 編程的動(dòng)態(tài)性,用 BCEL 設(shè)計(jì)字節(jié)碼
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,870評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,742評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,527評(píng)論 6 407
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,010評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,250評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,656評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,853評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,103評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,472評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,717評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,487評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,815評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容