一起學(xué)Java虛擬機(jī)(二):類文件結(jié)構(gòu)

一起學(xué)Java虛擬機(jī)系列

前言

了解JVM是對Java程序員的基本要求,但是有多少同學(xué)和我有一樣醉心解bug堆布局,忘記了內(nèi)功修煉,對JVM的理解是零碎的。系統(tǒng)地學(xué)習(xí)一次JVM也許能讓我們在這條路走得更好更遠(yuǎn)。

無關(guān)性

平臺無關(guān)性

  • “一次編寫,到處運(yùn)行(Write Once,Run Anywhere)”
  • 各種不同平臺的Java虛擬機(jī),以及所有平臺都統(tǒng)一支持的程序存儲格式——字節(jié)碼(Byte Code)

語言無關(guān)性

  • Java虛擬機(jī)不與包括Java語言在內(nèi)的任何程序語言綁定,它只與“Class文件”這種特定的二進(jìn)制文件格式所關(guān)聯(lián),Class文件中包含了Java虛擬機(jī)指令集、符號表以及若干其他輔助信息。
  • Java的規(guī)范拆分成了《Java語言規(guī)范》(The Java Language Specification)及《Java虛擬機(jī)規(guī)范(The Java Virtual Machine Specification)兩部分
  • 作為一個通用的、與機(jī)器無關(guān)的執(zhí)行平臺,任何其他語言的實(shí)現(xiàn)者都可以將Java虛擬機(jī)作為他們語言的運(yùn)行基礎(chǔ),以Class文件作為他們產(chǎn)品的交付媒介。
image.png

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

Class文件是一組以8個字節(jié)為基礎(chǔ)單位的二進(jìn)制流,各個數(shù)據(jù)項(xiàng)目嚴(yán)格按照順序緊湊地排列在文
件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內(nèi)容幾乎全部是程序運(yùn)行的必要數(shù)
據(jù),沒有空隙存在。當(dāng)遇到需要占用8個字節(jié)以上空間的數(shù)據(jù)項(xiàng)時,則會按照高位在前(大端表示法(big-endian))的方式分割成若干個8個字節(jié)進(jìn)行存儲。

“無符號數(shù)”和"表"

根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,Class文件格式采用一種類似于C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲數(shù)
據(jù),這種偽結(jié)構(gòu)中只有兩種數(shù)據(jù)類型:“無符號數(shù)”和“表”。

  • 無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1、u2、u4、u8來分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù),無符號數(shù)可以用來描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值。
  • 表是由多個無符號數(shù)或者其他表作為數(shù)據(jù)項(xiàng)構(gòu)成的復(fù)合數(shù)據(jù)類型,為了便于區(qū)分,所有表的命名都習(xí)慣性地以“_info”結(jié)尾。表用于描述有層次關(guān)系的復(fù)合結(jié)構(gòu)的數(shù)據(jù),整個Class文件本質(zhì)上也可以視作是一張表。

使用010Editor查看class文件結(jié)構(gòu)

通過010Editor查看HelloWorld.class

public class HelloWorld {

    private static final String HELLO_WORLD = "Hello World!";
    public static void main(String args[]) {
        System.out.println(HELLO_WORLD);
    }
}

下載地址:

https://www.sweetscape.com/download/010editor/

下載完成后打開class文件會自動提示安裝CLASSAdv.bt插件

image.png

魔數(shù)

  • magic
    每個Class文件的頭4個字節(jié)被稱為魔數(shù)(Magic Number),它的唯一作用是確定這個文件是否為
    一個能被虛擬機(jī)接受的Class文件,Class文件的魔數(shù)取得很有“浪漫氣息”,值為0xCAFEBABE
image.png

版本號

  • minorersion 和 major_version
    緊接著魔數(shù)的4個字節(jié)存儲的是Class文件的版本號:第5和第6個字節(jié)是次版本號(Minor Version),第7和第8個字節(jié)是主版本號(Major Version)

    每個版本的 JDK 都有自己特定的版本號。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能運(yùn)行高版本的 Class 文件,即使文件格式?jīng)]有發(fā)生任何變化,虛擬機(jī)也拒絕執(zhí)行高于其版本號的 Class 文件
52對應(yīng)jdk8

常量池

  • u2 constant_pool_count
    由于常量池中常量的數(shù)量是不固定的,所以在常量池的入口需要放置一項(xiàng)u2類型的數(shù)據(jù),代表常
    量池容量計數(shù)值(constant_pool_count)

image.png


需要注意的是,常量池的下標(biāo)是從 1 開始的,也就代表該 Class 文件具有 36 個常量。那么,為什么下標(biāo)要從 1 開始呢?目的是為了表示在特定情況下 不引用任何一個常量池項(xiàng),這時候下標(biāo)就用 0 表示。

  • u2 constant_pool

常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References),常量池中每一項(xiàng)常量都是一個表。

常量池的數(shù)據(jù)類型有十幾種,各自都有自己的數(shù)據(jù)結(jié)構(gòu),但是他們都有一個共有屬性 tag。tag 是標(biāo)志位,標(biāo)記是哪一種數(shù)據(jù)結(jié)構(gòu)。

常見常量池類型:

類 型 標(biāo) 志 描 述
CONSTANT_Utf8_info 1 UTF-8 編碼的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮點(diǎn)型字面量
COSTANT_Long_info 5 長整型字面量
CONSTANT_Double_info 6 雙精度浮點(diǎn)型字面量
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_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 標(biāo)識方法類型
CONSTANT_InvokeDynamic_info 18 表示一個動態(tài)方法調(diào)用點(diǎn)
CONSTANT_Module_ino 19 表示一個模塊
CONSTANT_Package_ino 20 表示一個模塊開或者導(dǎo)出的包

我們再來粗略解析一下常量池的第一項(xiàng)常量


image.png
  • tag :10 代表類型CONSTANT_Methodref_info,類中方法的符號引用
  • classIndex : 6 這個是一個常量池索引, 代表第5個數(shù)據(jù)項(xiàng)(CONSTANT_Methodref_info 的 class_index 指向的數(shù)據(jù)項(xiàng)永遠(yuǎn)是 CONSTANT_Class_info)
  • name_and_type_index :23 同樣是一個常量池索引

先來看:classIndex:

image.png
  • tag :7代表CONSTANT_Class_info
  • name_index :30 又是一個常量池索引
image.png
  • 數(shù)據(jù)項(xiàng)29代表了一個CONSTANT_Utf8_info類型,bytes[16]里是字符串的內(nèi)容,從 010Editor 解析內(nèi)容可以看到這個字符串是 java/lang/System,表示類的權(quán)限定名

回頭在來看第一個常量的 name_and_type_index

image.png
  • tag : 12 代表CONSTANT_NameAndType_info
  • name_index 表示字段或者方法的非限定名,這里的值是 <init>
  • descriptor_index表示字段描述符或者方法描述符,這里的值是 ()V。
image.png

這樣,常量池的第一個數(shù)據(jù)項(xiàng)就分析完了,后面的每一個數(shù)據(jù)項(xiàng)都可以按照這樣分析

javap工具

剩余部分全部手動分析太雷,我們偷個懶。在JDK的bin目錄中,Oracle公司已經(jīng)為我們
準(zhǔn)備好一個專門用于分析Class文件字節(jié)碼的工具:javap

HP-ProDesk-680-G6-PCI-Microtower-PC:~/DEBUG$ javap -verbose HelloWorld.class 
Classfile /home/mi/DEBUG/HelloWorld.class
  Last modified May 12, 2021; size 641 bytes
  MD5 checksum 1910a4531e5743c190636067d43d4bc4
  Compiled from "HelloWorld.java"
public class com.wang.javavmdemo.HelloWorld
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // com/wang/javavmdemo/HelloWorld
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #6.#23         // java/lang/Object."<init>":()V
   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #26            // com/wang/javavmdemo/HelloWorld
   #4 = String             #27            // Hello World!
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #30            // java/lang/Object
   #7 = Utf8               HELLO_WORLD
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               ConstantValue
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/wang/javavmdemo/HelloWorld;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               SourceFile
  #22 = Utf8               HelloWorld.java
  #23 = NameAndType        #10:#11        // "<init>":()V
  #24 = Class              #31            // java/lang/System
  #25 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #26 = Utf8               com/wang/javavmdemo/HelloWorld
  #27 = Utf8               Hello World!
  #28 = Class              #34            // java/io/PrintStream
  #29 = NameAndType        #35:#36        // println:(Ljava/lang/String;)V
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (Ljava/lang/String;)V
{
  public com.wang.javavmdemo.HelloWorld();
    descriptor: ()V
    flags: (0x0001) 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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wang/javavmdemo/HelloWorld;

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


類文件的剩余部分,先預(yù)覽一下:

image.png

訪問標(biāo)志

  • access_flags
    在常量池結(jié)束之后,緊接著的2個字節(jié)代表訪問標(biāo)志(access_flags),這個標(biāo)志用于識別一些類或
    者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義為public類型;是否定義為abstract
    類型;如果是類的話,是否被聲明為final;等等

標(biāo)志位及含義表:

標(biāo)志名稱 標(biāo) 志 值 含 義
ACC_PUBIC 0x0001 是否為 public 類型
ACC_FINAL 0x0010 是否聲明為 final
ACC_SUPER 0x0020 JDK1.0.2 之后編譯出來的類這個標(biāo)志都必須為真
ACC_INTERFACE 0x0200 是否為接口
ACC_ABSTRACT 0x0400 是否為 abstract 類型
ACC_SYNTHETIC 0x1000 標(biāo)記這個類并非由用戶代碼產(chǎn)生
ACC_ANNOTATION 0x2000 是否為注解
ACC_ENUM 0x4000 是否為枚舉類型
ACC_MODULE 0x8000 是否為模塊

HelloWorld是一個普通類,不是接口、注解、枚舉類型或者莫模塊,并且被public關(guān)鍵字修飾
因此他的access_flag應(yīng)該為ACC_PUBIC|ACC_SUPER,轉(zhuǎn)換為10進(jìn)制就是33

類索引、父類索引與接口索引集合

Class文件中由這三項(xiàng)數(shù)據(jù)來確定該類型的繼承關(guān)系。

  • this_class類索引用于確定這個類的全限定名。value指向常量池中的索引。
  • super_class父類索引用于確定這個類的父類的全限定名。由于Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的Java類都有父類,因此除了java.lang.Object外,所有Java類的父類索引都不為0。
  • interfaces_count,表示的是該類實(shí)現(xiàn)的接口數(shù)量。HelloWorld未實(shí)現(xiàn)任何接口所以是0。
  • 如果實(shí)現(xiàn)了若干接口,這些接口信息將存儲在之后的 interfaces[] 之中。接口索引集合就用來描述這個類實(shí)現(xiàn)了哪些接口,這些被實(shí)現(xiàn)的接口將按implements關(guān)鍵字(如果這個Class文件表示的是一個接口,則應(yīng)當(dāng)是extends關(guān)鍵字)后的接口順序從左到右排列在接口索引集合中。

字段表長度和字段表集合

Java語言中的“字段”(Field)包括類級變量以及實(shí)例級變量,但不包括在方法內(nèi)部聲明的局部變量。

  • fileds_count 表示該類中聲明的變量個數(shù)
  • filed_info 表示該類中聲明的變量信息

private static final String HELLOWORLD = "HelloWorld";

可以看出來,Java中描述一個字段首先是訪問范圍,是公有的還是私有的,或者受保護(hù)的,這個信息決定了字段是否堆特定范圍的類可見。
其次是一些關(guān)鍵字修飾的描述信息,是實(shí)例變量還是類變量,是否可變,并發(fā)可見性,是否可被序列化等,這些關(guān)鍵字包括static、final 、volatile、transient等。
在后面便是字段的數(shù)據(jù)類型(基本數(shù)據(jù)類型、數(shù)組、對象)和名稱。
上述的這些修飾符都是用布爾值來描述的,而數(shù)據(jù)類型和名稱都是不確定的,通常引用常量池的常量來描述。

image.png

方法表長度和方法表集合

Class文件存儲
格式中對方法的描述與對字段的描述采用了幾乎完全一致的方式,方法表的結(jié)構(gòu)如同字段表一樣,依
次包括訪問標(biāo)志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表
集合(attributes)幾項(xiàng)

image.png

方法的定義可以通過訪問標(biāo)志、名稱和描述符索引來表述清楚,那么方法中的代碼又在哪里呢?

方法里的java代碼經(jīng)過編譯器編譯成字節(jié)碼指令后,存放在方法的屬性表集合里一個名為“Code”的屬性里。

image.png

attribute_name_index 對應(yīng)一個類型為CONSTANT_Utf8_info的常量索引,常量值固定為“Code”

屬性表長度和屬性表集合

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


image.png

與class文件其它數(shù)據(jù)項(xiàng)目嚴(yán)格要求順序長度不同,屬性表集合限制相對比較寬松,不要求各個屬性表具有嚴(yán)格順序,只要不與已有屬性名重復(fù),任何人實(shí)現(xiàn)的編譯器均可向?qū)傩员韺懭胱约旱膶傩?,jvm運(yùn)行時會自動忽略掉不認(rèn)識的屬性。

java7中定義的屬性如下表:

屬性名稱 使用位置 含義
Code 方法表 Java代碼編譯成的字節(jié)碼指令
ConstantValue 字段表 final關(guān)鍵字定義的常量池
Deprecated 類,方法,字段表 被聲明為deprecated的方法和字段
Exceptions 方法表 方法拋出的異常
EnclosingMethod 類文件 僅當(dāng)一個類為局部類或者匿名類是才能擁有這個屬性,這個屬性用于標(biāo)識這個類所在的外圍方法
InnerClass 類文件 內(nèi)部類列表
LineNumberTable Code屬性 Java源碼的行號與字節(jié)碼指令的對應(yīng)關(guān)系
LocalVariableTable Code屬性 方法的局部便狼描述
StackMapTable Code屬性 JDK1.6中新增的屬性,供新的類型檢查檢驗(yàn)器檢查和處理目標(biāo)方法的局部變量和操作數(shù)有所需要的類是否匹配
Signature 類,方法表,字段表 用于支持泛型情況下的方法簽名
SourceFile 類文件 記錄源文件名稱
SourceDebugExtension 類文件 用于存儲額外的調(diào)試信息
Synthetic 類,方法表,字段表 標(biāo)志方法或字段為編譯器自動生成的
LocalVariableTypeTable 使用特征簽名代替描述符,是為了引入泛型語法之后能描述泛型參數(shù)化類型而添加
RuntimeVisibleAnnotations 類,方法表,字段表 為動態(tài)注解提供支持
RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是運(yùn)行時不可見的
RuntimeVisibleParameterAnnotation 方法表 作用與RuntimeVisibleAnnotations屬性類似,只不過作用對象為方法
RuntimeInvisibleParameterAnnotation 方法表 作用與RuntimeInvisibleAnnotations屬性類似,作用對象哪個為方法參數(shù)
AnnotationDefault 方法表 用于記錄注解類元素的默認(rèn)值
BootstrapMethods 類文件 用于保存invokeddynamic指令引用的引導(dǎo)方式限定符

關(guān)于字段表集合、屬性表集合、方法表集合的結(jié)構(gòu) 以及acess_flag的列表
可以像查字典一樣查閱《深入理解JAVA虛擬機(jī)》6.3小節(jié)的內(nèi)容。我們只需要理解原理。

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

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