🔥JVM從入門到入土之JVM的類文件結構

前言

文本已收錄至個人GitHub倉庫,歡迎Star:github.com/bin39232820…
種一棵樹最好的時間是十年前,其次是如今
我知道不少人不玩qq了,可是懷舊一下,歡迎加入六脈神劍Java菜鳥學習羣,羣聊號碼:549684836 鼓勵你們在技術的路上寫博客java

絮叨

昨天講了類加載機制,其實那個應該算是第二步,第一步仍是咱們的.Class文件的結構,可是直接講這個未免太枯燥,因此我就寫講了類加載機制,再講文件結構git

咱們知道咱們寫完的Java程序通過javac xxx.java編譯後生成了xxx.class文件,但是你是否想過xxx.class文件究竟是什麼?這個文件中到底包含了什麼內容?那麼如今咱們就一塊兒經過解析一個.class文件來深刻的學習一下類文件結構,經過此次的學習,我想你會對class文件瞭如指掌。github

Class類文件結構

在解析一個class文件以前,咱們須要先學習一下Class類文件的結構,這個類文件結構至關於一個總綱,咱們立刻就會對照着這個類文件結構解析真正的class文件。數組

  • Class文件是一組以8個字節爲基礎單位的二進制流(多是磁盤文件,也多是類加載器直接生成的),各個數據項目嚴格按照順序- 緊湊地排列,中間沒有任何分隔符;
  • Class文件格式採用一種相似於C語言結構體的僞結構來存儲數據,其中只有兩種數據類型:無符號數和表;
  • 無符號數屬於基本的數據類型,以u一、u二、u4和u8來分別表明1個字節、2個字節、4個字節和8個字節的無符號數,能夠用來描述數字- 、索引引用、數量值或者按照UTF-8編碼構成字符串值;
  • 表是由多個無符號數獲取其餘表做爲數據項構成的複合數據類型,習慣以「_info」結尾;
  • 不管是無符號數仍是表,當須要描述同一個類型但數量不定的多個數據時,常常會使用一個前置的容量計數器加若干個連續的數據項- 的形式,這時稱這一系列連續的某一類型的數據未某一類型的集合。

類文件結構圖:bash

類文件分析

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

咱們經過16進制編輯器打開編譯後的HelloWorld.class文件,其十六進制的文件內容以下:markdown

魔數和版本

  • Class文件的頭4個字節,惟一做用是肯定文件是否爲一個可被虛擬機接受的Class文件,固定爲「0xCAFEBABE」。
  • 第5和第6個字節是次版本號,第7和第8個字節是主版本號(0x0034爲52,對應JDK版本1.8);Java的版本號是從45開始的,JDK1.1以後的每個JDK大版本發佈主版本號向上加1,高版本的JDK能向下兼容低版本的JDK。

對應到class文件中就是:數據結構

圖中1就是魔數,第二個就是版本

常量池

緊接着主版本號的就是常量池,常量池能夠理解爲class文件的資源倉庫,它是class文件結構中與其它項目關聯最多的數據類型,也是佔用class文件空間最大的數據項目之一,也是class文件中第一個出現的表類型數據項目。併發

因爲常量池中常量的數量不是固定的,因此常量池入口須要放置一項u2類型的數據,表明常量池中的容量計數。不過,這裏須要注意的是,這個容器計數是從1開始的而不是從0開始,也就是說,常量池中常量的個數是這個容器計數-1。將0空出來的目的是知足後面某些指向常量池的索引值的數據在特定狀況下須要表達「不引用任何一個常量池項目」的含義。class文件中只有常量池的容量計數是從1開始的,對於其它集合類型,好比接口索引集合、字段表集合、方法表集合等的容量計數都是從0開始的。編輯器

常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近Java語言的常量概念,如文本字符串、聲明爲final的常量等。而符號引用則屬於編譯原理方面的概念,它包括三方面的內容:工具

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

Java代碼在進行javac編譯的時候並不像C和C++那樣有鏈接這一步,而是在虛擬機加載class文件的時候進行動態鏈接。也就是說,在class文件中不會保存各個方法、字段的最終內存佈局信息,所以這些字段、方法的符號引用不通過運行期轉換的話沒法獲得真正的內存入口地址,虛擬機也就沒法使用。當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立時或運行時解析、翻譯到具體的內存地址中。

常量池中的每一項都是一個表,在JDK1.7以前有11中結構不一樣的表結構,在JDK1.7中爲了更好的支持動態語言調用,又增長了3種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。不過這裏不會介紹這三種表數據結構。

這14個表的開始第一個字節是一個u1類型的tag,用來標識是哪種常量類型。這14種常量類型所表明的含義以下:

由class文件結構圖可知:

常量池的開頭兩個字節0x0022是常量池的容量計數,這裏是34,也就是說,這個常量池中有33個常量項。 咱們能夠看一下這33個常量:

藍色部分的內容就是33個常量,咱們能夠發現圖片右邊用UTF-8編碼後已經把常量翻譯成了英文字母。能夠看到這部分的內容很是多。由於常量池中的常量比較多,每一中常量還有本身的結構,致使常量池的結構很是複雜,這裏只解析第一個常量做爲示例:

看看這個例子的第一項,容量計數後面的第一個字節標識這個常量的類型,是0x0A,即10,查表可知是類方法的符號引用,這個常量表的結構以下:

按照這個結構,能夠知道name_index是6(0x0006),descriptor_index是20(0x0014)。這都是一個索引,指向常量池中的其餘常量,其中name描述了這個方法的名稱,descriptor描述了這個方法的訪問標誌(好比public、private等)、參數類型和返回類型。(這裏由於手工解析常量池確實是一件很坑爹的工做,並且後面會介紹自動解析的工具,因此這裏就不去管name和descriptor的內容了)

咱們能夠看到手工解析常量池是一件很是痛苦的事情,這裏還只是一個特別簡單的例子生成的class文件,咱們能夠本身想一想若是是本身寫的一個程序編譯爲class文件後,它的常量池會很是大,因此Java已經爲咱們提供了一個解析常量池的工具javap,咱們能夠經過javap -verbose class文件名,就能夠自動幫咱們解析了,下面是這個程序的解析結果:

警告: 二進制文件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"
複製代碼

訪問標誌

常量池結束後緊接着的兩個字節表明訪問標誌,用來標識一些類或接口的訪問信息,包括:這個Class是類仍是接口;是否認義爲public;是否認義爲abstract;若是是類的話,是否被聲明爲final等。具體的標誌位以及含義以下表:

因爲access_flags是兩個字節大小,一共有十六個標誌位可使用,當前僅僅定義了8個,沒有用到的標誌位都是0。對於一個類來講,可能會有多個訪問標誌,這時就能夠對照上表中的標誌值取或運算的值。拿上面那個例子來講,它的訪問標誌值是0x0021,查表可知,這是ACC_PUBLIC和ACC_SUPER值取或運算的結果。因此HelloWorld這個類的訪問標誌就是ACC_PUBLIC和ACC_SUPER,這一點咱們能夠在javap獲得的結果中驗證:

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

在訪問標誌access_flags後接下來就是類索引(this_class)和父類索引(super_class),這兩個數據都是u2類型的,而接下來的接口索引集合是一個u2類型的集合,class文件由這三個數據項來肯定類的繼承關係。因爲Java中是單繼承,因此父類索引只有一個;但Java類能夠實現多個接口,因此接口索引是一個集合。

類索引用來肯定這個類的全限定名,這個全限定名就是說一個類的類名包含全部的包名,而後使用」/」代替」.」。好比Object的全限定名是java.lang.Object。父類索引肯定這個類的父類的全限定名,除了Object以外,全部的類都有父類,因此除了Object以外全部類的父類索引都不爲0.接口索引集合存儲了implements語句後面按照從左到右的順序的接口。

類索引和父類索引都是一個索引,這個索引指向常量池中的CONSTANT_Class_info類型的常量。而後再CONSTANT_Class_info常量中的索引就能夠找到常量池中類型爲CONSTANT_Utf8_info的常量,而這個常量保存着類的全限定名。

字段表集合

字段表集合,顧名思義就是Java類中的字段,字段又分爲類字段(靜態屬性)和實例字段(對象屬性),那麼,在Class文件中是如何保存這些字段的呢?咱們能夠想想保存一個字段須要保存它的哪些信息呢?

答案是:字段的做用域(public、private和protected修飾符)、是實例變量仍是類變量(static修飾符)、可變性(final修飾符)、併發可見性(volatile修飾符)、是否可被序列化(transient修飾符)、字段的數據類型(基本類型、對象、數組)以及字段名稱。

方法表集合

在字段表集合中介紹了字段的描述符和方法的描述符,對於理解方法表有很大幫助。class文件存儲格式中對方法的描述和對字段的描述幾乎相同,方法表的結構也和字段表相同,這裏就再也不列出。不過,方法表的訪問標誌和字段的不一樣,列出以下:

屬性表集合

屬性表在前面出現了屢次,在Class文件、字段表和方法表均可以攜帶本身的屬性表集合,來描述某些場景專有的信息。 與Class文件中其餘的數據項目要求嚴格的順序、長度和內容不一樣,屬性表集合的限制比較少,不要求嚴格的順序,只要不與已有的屬性名重複,任何人實現的編譯器均可以向屬性表中寫入自定義的屬性信息,Java虛擬機會在運行時忽略掉那些不認識的信息。爲了能正確解析class文件,《Java虛擬機規範(第二版)》中預約義了9項虛擬機應當識別的屬性。如今,屬性已經達到了21項。具體信息以下表,這裏僅對常見的屬性作介紹:

結尾

其實真心不想寫這篇的,由於本身也沒有靜下心來,認真的一個個本身去實際,只是說把書上的東西搬過來,這個坑之後補吧,可能對字節碼的東西仍是剛接觸,等有了最基本的機率再去啃它,太難了

平常求贊

好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是真粉

創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見

六脈神劍 | 文 【原創】若是本篇博客有任何錯誤,請批評指教,不勝感激 !

相關文章
相關標籤/搜索