細(xì)說(shuō)JVM(類文件結(jié)構(gòu)(一))

一、前言

我們知道我們寫完的Java程序經(jīng)過(guò)javac xxx.java編譯后生成了xxx.class文件,可是你是否想過(guò)xxx.class文件到底是什么?這個(gè)文件中到底包含了什么內(nèi)容?那么現(xiàn)在我們就一起通過(guò)解析一個(gè).class文件來(lái)深入的學(xué)習(xí)一下類文件結(jié)構(gòu),通過(guò)這次的學(xué)習(xí),我想你會(huì)對(duì)class文件了如指掌。

二、Class類文件結(jié)構(gòu)

在解析一個(gè)class文件之前,我們需要先學(xué)習(xí)一下Class類文件的結(jié)構(gòu),這個(gè)類文件結(jié)構(gòu)相當(dāng)于一個(gè)總綱,我們馬上就會(huì)對(duì)照著這個(gè)類文件結(jié)構(gòu)解析真正的class文件。

  • Class文件是一組以8個(gè)字節(jié)為基礎(chǔ)單位的二進(jìn)制流(可能是磁盤文件,也可能是類加載器直接生成的),各個(gè)數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列,中間沒有任何分隔符;
  • Class文件格式采用一種類似于C語(yǔ)言結(jié)構(gòu)體的偽結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù),其中只有兩種數(shù)據(jù)類型:無(wú)符號(hào)數(shù)和表;
  • 無(wú)符號(hào)數(shù)屬于基本的數(shù)據(jù)類型,以u(píng)1、u2、u4和u8來(lái)分別代表1個(gè)字節(jié)、2個(gè)字節(jié)、4個(gè)字節(jié)和8個(gè)字節(jié)的無(wú)符號(hào)數(shù),可以用來(lái)描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值;
  • 表是由多個(gè)無(wú)符號(hào)數(shù)獲取其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型,習(xí)慣以“_info”結(jié)尾;
  • 無(wú)論是無(wú)符號(hào)數(shù)還是表,當(dāng)需要描述同一個(gè)類型但數(shù)量不定的多個(gè)數(shù)據(jù)時(shí),經(jīng)常會(huì)使用一個(gè)前置的容量計(jì)數(shù)器加若干個(gè)連續(xù)的數(shù)據(jù)項(xiàng)的形式,這時(shí)稱這一系列連續(xù)的某一類型的數(shù)據(jù)未某一類型的集合。

類文件結(jié)構(gòu)圖:

class_file_format.png

三、類文件分析

我們就以一個(gè)非常經(jīng)典的代碼作為例子進(jìn)行分析,代碼如下:

package temp;
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello,World");
    }
}

我們通過(guò)16進(jìn)制編輯器打開編譯后的HelloWorld.class文件,其十六進(jìn)制的文件內(nèi)容如下:

TIM截圖20180803171743.png

1、魔數(shù)和版本
  • Class文件的頭4個(gè)字節(jié),唯一作用是確定文件是否為一個(gè)可被虛擬機(jī)接受的Class文件,固定為“0xCAFEBABE”。
  • 第5和第6個(gè)字節(jié)是次版本號(hào),第7和第8個(gè)字節(jié)是主版本號(hào)(0x0034為52,對(duì)應(yīng)JDK版本1.8);Java的版本號(hào)是從45開始的,JDK1.1之后的每一個(gè)JDK大版本發(fā)布主版本號(hào)向上加1,高版本的JDK能向下兼容低版本的JDK。

對(duì)應(yīng)到class文件中就是:


TIM截圖20180803171820.png
2、常量池

緊接著主版本號(hào)的就是常量池,常量池可以理解為class文件的資源倉(cāng)庫(kù),它是class文件結(jié)構(gòu)中與其它項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù)類型,也是占用class文件空間最大的數(shù)據(jù)項(xiàng)目之一,也是class文件中第一個(gè)出現(xiàn)的表類型數(shù)據(jù)項(xiàng)目。

由于常量池中常量的數(shù)量不是固定的,所以常量池入口需要放置一項(xiàng)u2類型的數(shù)據(jù),代表常量池中的容量計(jì)數(shù)。不過(guò),這里需要注意的是,這個(gè)容器計(jì)數(shù)是從1開始的而不是從0開始,也就是說(shuō),常量池中常量的個(gè)數(shù)是這個(gè)容器計(jì)數(shù)-1。將0空出來(lái)的目的是滿足后面某些指向常量池的索引值的數(shù)據(jù)在特定情況下需要表達(dá)“不引用任何一個(gè)常量池項(xiàng)目”的含義。class文件中只有常量池的容量計(jì)數(shù)是從1開始的,對(duì)于其它集合類型,比如接口索引集合、字段表集合、方法表集合等的容量計(jì)數(shù)都是從0開始的。

常量池中主要存放兩大類常量:字面量和符號(hào)引用。字面量比較接近Java語(yǔ)言的常量概念,如文本字符串、聲明為final的常量等。而符號(hào)引用則屬于編譯原理方面的概念,它包括三方面的內(nèi)容:

  • 類和接口的全限定名(Fully Qualified Name);
  • 字段的名稱和描述符(Descriptor);
  • 方法的名稱和描述符;

Java代碼在進(jìn)行javac編譯的時(shí)候并不像C和C++那樣有連接這一步,而是在虛擬機(jī)加載class文件的時(shí)候進(jìn)行動(dòng)態(tài)連接。也就是說(shuō),在class文件中不會(huì)保存各個(gè)方法、字段的最終內(nèi)存布局信息,因此這些字段、方法的符號(hào)引用不經(jīng)過(guò)運(yùn)行期轉(zhuǎn)換的話無(wú)法得到真正的內(nèi)存入口地址,虛擬機(jī)也就無(wú)法使用。當(dāng)虛擬機(jī)運(yùn)行時(shí),需要從常量池獲得對(duì)應(yīng)的符號(hào)引用,再在類創(chuàng)建時(shí)或運(yùn)行時(shí)解析、翻譯到具體的內(nèi)存地址中。

常量池中的每一項(xiàng)都是一個(gè)表,在JDK1.7之前有11中結(jié)構(gòu)不同的表結(jié)構(gòu),在JDK1.7中為了更好的支持動(dòng)態(tài)語(yǔ)言調(diào)用,又增加了3種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。不過(guò)這里不會(huì)介紹這三種表數(shù)據(jù)結(jié)構(gòu)。

這14個(gè)表的開始第一個(gè)字節(jié)是一個(gè)u1類型的tag,用來(lái)標(biāo)識(shí)是哪一種常量類型。這14種常量類型所代表的含義如下:


class_file_format.png

由class文件結(jié)構(gòu)圖可知:


TIM截圖20180803173043.png

常量池的開頭兩個(gè)字節(jié)0x0022是常量池的容量計(jì)數(shù),這里是34,也就是說(shuō),這個(gè)常量池中有33個(gè)常量項(xiàng)。
我們可以看一下這33個(gè)常量:


TIM截圖20180803173351.png

藍(lán)色部分的內(nèi)容就是33個(gè)常量,我們可以發(fā)現(xiàn)圖片右邊用UTF-8編碼后已經(jīng)把常量翻譯成了英文字母??梢钥吹竭@部分的內(nèi)容非常多。因?yàn)槌A砍刂械某A勘容^多,每一中常量還有自己的結(jié)構(gòu),導(dǎo)致常量池的結(jié)構(gòu)非常復(fù)雜,這里只解析第一個(gè)常量作為示例:

看看這個(gè)例子的第一項(xiàng),容量計(jì)數(shù)后面的第一個(gè)字節(jié)標(biāo)識(shí)這個(gè)常量的類型,是0x0A,即10,查表可知是類方法的符號(hào)引用,這個(gè)常量表的結(jié)構(gòu)如下:

類型 名稱 數(shù)量
U1 tag 1
U2 name_index 1
U2 descriptor_index 1

按照這個(gè)結(jié)構(gòu),可以知道name_index是6(0x0006),descriptor_index是20(0x0014)。這都是一個(gè)索引,指向常量池中的其他常量,其中name描述了這個(gè)方法的名稱,descriptor描述了這個(gè)方法的訪問標(biāo)志(比如public、private等)、參數(shù)類型和返回類型。(這里因?yàn)槭止そ馕龀A砍卮_實(shí)是一件很坑爹的工作,而且后面會(huì)介紹自動(dòng)解析的工具,所以這里就不去管name和descriptor的內(nèi)容了)
我們可以看到手工解析常量池是一件非常痛苦的事情,這里還只是一個(gè)特別簡(jiǎn)單的例子生成的class文件,我們可以自己想想如果是自己寫的一個(gè)程序編譯為class文件后,它的常量池會(huì)非常大,所以Java已經(jīng)為我們提供了一個(gè)解析常量池的工具javap,我們可以通過(guò)javap -verbose class文件名,就可以自動(dòng)幫我們解析了,下面是這個(gè)程序的解析結(jié)果:

I:\work\out\production\work\temp>javap -verbose HelloWorld
警告: 二進(jìn)制文件HelloWorld包含temp.HelloWorld
Classfile /I:/work/out/production/work/temp/HelloWorld.class
  Last modified 2018-8-3; size 543 bytes
  MD5 checksum 5eeb0ca06c253d3206781e81895bd4a4
  Compiled from "HelloWorld.java"
public class temp.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello,World
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // temp/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Ltemp/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello,World
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               temp/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public temp.HelloWorld();
    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 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltemp/HelloWorld;

  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 Hello,World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"

這里我們先不要管后面的內(nèi)容,我們只看常量池部分,很明顯,33個(gè)常量已經(jīng)被解析完畢,現(xiàn)在我們可以看一下第一個(gè)常量的內(nèi)容:
[圖片上傳失敗...(image-8b7582-1533291668097)]
我們可以發(fā)現(xiàn)第一個(gè)常量指向了第6個(gè)和第20個(gè)常量,經(jīng)過(guò)分析其指向的常量,最終的結(jié)果是后面顯示的java/lang/Object."<init>":()V,我們現(xiàn)在對(duì)這個(gè)字符串所表示的內(nèi)容大概有自己的猜測(cè),不過(guò)也有自己的疑惑之處,這都不要緊,因?yàn)楹竺嫖覀兙蜁?huì)分析相似的字符串的意思。而且我們會(huì)發(fā)現(xiàn)<init>并沒有在Java程序中出現(xiàn),還有一些內(nèi)容也沒有在Java程序中出現(xiàn),比如“[”、“V”、“LineNumberTable”等。這是自動(dòng)生成的常量,但它們會(huì)被后面即將介紹到的字段表、方法表和屬性表引用到,用來(lái)描述一些不方便使用固定字節(jié)表示的內(nèi)容。譬如描述方法的返回值是什么?有幾個(gè)參數(shù)?每個(gè)參數(shù)的類型是什么?

最后,給出14種常量項(xiàng)的結(jié)構(gòu):


class_file_constant_pool_detail1.png
class_file_constant_pool_detail2.png

HelloWorld這個(gè)類的訪問標(biāo)志就是ACC_PUBLICACC_SUPER,這一點(diǎn)我們可以在javap得到的結(jié)果中驗(yàn)證:

TIM截圖20180803181012.png

這篇博客就先分析到訪問標(biāo)志,因?yàn)楹竺娴膬?nèi)容還有很多,考慮到一篇很長(zhǎng)的文章會(huì)極大的降低閱讀體驗(yàn),所以類文件結(jié)構(gòu)這篇文章就分為兩章。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容