jvm中類和對象定義存儲基礎知識 | 京東雲技術團隊

1 類文件數據結構類型

Class文件結構主要有兩種數據結構:無符號數和表

無符號數:用來表述數字,索引引用、數量值以及字符串等,比如 圖1中類型爲u1,u2,u4,u8分別代表1個字節,2個字節,4個字節,8個字節的無符號數

:表是有由多個無符號數以及其它的表組成的複合結構,比如圖1中類型以_info結尾的項爲表類型。

2 類結構定義

Class類文件是緊湊、順序、無空隙的,魔數(MagicNumber)、Class文件版本(Version)、常量池(Constant_Pool)、訪問標記(Access_flag)、本類(This_class)、父類(Super_class)、接口(Interfaces)、字段集合(Fields)、方法集合(Methods )、屬性集合(Attributes)。其中因爲java多繼承所以interfaces接口類型爲數組;attribute_info則是方法表中定義的code索引,指向具體的方法體字節碼。如圖1所示。

下面用一段程序做說明,此類有接口,有方法、類變量和實例變量,機器是如何識別字節碼然後按照上面的規則來定義此class類呢?

package com.jd.crm.Logback;

public class TestClass implements Super{

    private static final int staticVar = 0;

    private int instanceVar=0;

    public int instanceMethod(int param) throws  Exception{
        return param ++;
    }
}
interface Super{ }

通過javap幫助解析class文件格式如下:

Classfile /D:/spm-workspace/test/target/classes/com/jd/crm/Logback/TestClass.class
  Last modified 2023-4-14; size 597 bytes
  MD5 checksum 9d5dd9fc2145ac17393fee7a707d3b9c
  Compiled from "TestClass.java"
public class com.jd.crm.Logback.TestClass implements com.jd.crm.Logback.Super
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#26         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#27         // com/jd/crm/Logback/TestClass.instanceVar:I
   #3 = Class              #28            // com/jd/crm/Logback/TestClass
   #4 = Class              #29            // java/lang/Object
   #5 = Class              #30            // com/jd/crm/Logback/Super
   #6 = Utf8               staticVar
   #7 = Utf8               I
   #8 = Utf8               ConstantValue
   #9 = Integer            0
  #10 = Utf8               instanceVar
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/jd/crm/Logback/TestClass;
  #18 = Utf8               instanceMethod
  #19 = Utf8               (I)I
  #20 = Utf8               param
  #21 = Utf8               Exceptions
  #22 = Class              #31            // java/lang/Exception
  #23 = Utf8               MethodParameters
  #24 = Utf8               SourceFile
  #25 = Utf8               TestClass.java
  #26 = NameAndType        #11:#12        // "<init>":()V
  #27 = NameAndType        #10:#7         // instanceVar:I
  #28 = Utf8               com/jd/crm/Logback/TestClass
  #29 = Utf8               java/lang/Object
  #30 = Utf8               com/jd/crm/Logback/Super
  #31 = Utf8               java/lang/Exception
{
  public com.jd.crm.Logback.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field instanceVar:I
         9: return
      LineNumberTable:
        line 3: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/jd/crm/Logback/TestClass;

  public int instanceMethod(int) throws java.lang.Exception;
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iload_1
         1: iinc          1, 1
         4: ireturn
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jd/crm/Logback/TestClass;
            0       5     1 param   I
    Exceptions:
      throws java.lang.Exception
    MethodParameters:
      Name                           Flags
      param
}
SourceFile: "TestClass.java"

以上是javap幫助我們生成的class文件解析結果,只是給人看,而非機器。

通過編譯後生成class文件格式如下,因爲class文件是以8位作爲一個字節的二進制流。爲了方便計算,用16進製表示二進制(1個字節=2個十六進制的數,故下面每2個數就代表1個字節)

2.1 魔法數

前四個字節cafebabe是固定值,任何語言編譯成jvm認識的二進制流,前四位必須是固定的cafebabe字節。

2.2 版本號

緊接着2個字節00表示次版本號爲0 ;0034代表主版本爲52(jdk版本號對應的jdk版本爲1.8)參考jdk版本和class字節版本的對應關係

2.3 常量個數

常量個數const_pool_count字節碼爲00 20對應的說明常量個數爲32,實際爲31個,因爲首位jvm作爲保留位使用。

2.4 常量池

常量池存放兩大常量:字面量和符號引,字面量如文本字符串,被生命的final常量值等,而符號引用則包含類、接口的全限名稱、字段、方法名稱和描述符號等等。參考javap生成的類文件信息。

這裏只分析下其中一個常量,在上面常量個數2個字節後面緊接着一個字節0a十進制爲10,參考常量池類型10代表類中方法的符號引用。繼續參考方法類型MethodRef_info個格式定義:前兩個字節0004代表方法所在類名稱的索引,後兩個字節0001a代表一個NameAndType類型的索引。

2.5 類訪問標誌

緊接常量池定義完後的u2標識訪問標誌,本例標識爲0x0021和下圖標誌位按位或計算,如0x0001爲真,0x0020也爲真,其他爲否 最終確認訪問標誌位ACC_PUBLIC、ACC_SUPER

2.6 本類、父類、接口索引集合

根據圖1的規則,u2兩個字節0003標識當前類名的引用到,引用常量池數組下標爲#3,根據圖3所示子項的類名爲com/jd/crm/Logback/TestClass;0004代表父類類名的引用常量池數組下標爲#4,根據圖4所示引用的父類類名爲java/lang/Object;緊接着0001標識接口個數,指明數量爲1,0005標識第一個接口數組中接口的名稱,指向常量池中下標爲5的名稱爲com/jd/crm/Logback/Super;

比如查找當前類索引如下圖

2.7 字段表集合

字段表以數組的形式定義存儲在常量表中

以上圖說明,0002標識域個數爲2個域標識,在本類中有兩個,一個類的域字段staticVar 一個是實例對象的域字段instanceVar,如字段結構定義(下圖)定義,前2個字節001a爲訪問標識,和類訪問標識一樣,分別用001a的二進制和下圖字段域訪問標識類型做位或運算,得出訪問類型爲ACC_PRIVATE類型。name_index的佔用兩個字節0006,指向常量表下標爲6的引用,descriptor_index=0007指向常量表下標爲7的引用,此處爲I標識爲數據類型爲int,attributes_count=0001爲1個,值爲0008指向常量表下標爲#8的引用常量ConstantValue,標識爲靜態變量,最終依次類推第二個域標識引用

字段結構定義

字段域的訪問標誌請參考類訪問標誌,邏輯計算一致,只是規則不一樣而已 如下圖

2.8 方法表集合

和域字段集合表定義類似 也是數組方式定義在常量池中 ,其中方法的結構體第四個字段attributes_count代表方法的屬性數量,attribute_info就是屬性的集合參考屬性表集合

方法表訪問標識類型

通過上面方法的訪問標誌、名稱索引和描述索引定義方法的基本信息,方法的代碼塊則存放於類型爲Code的屬性表中。

2.9 屬性表集合

類、字段表、方法表本身可包含屬性表,屬性表格結構體如下,屬性表結構類型較多,比如有Code類型、Exception類型、MethodParameters類型等等,具體參考屬性表類型。所有的屬性都是引用常量池中的屬性類型名稱。然後根據屬性的長度指定該屬性的內容,根據屬性的不同類型解析不同的屬性值。格式定義如下

以Code屬性舉例,Code屬性結構如下所示

jvm按屬性獲取attribute_name_index指向常量池一個字符串常量Code,緊接着attribute_length標識Code類型Info信息長度,這個info內容包括:max_stack 最大棧深,max_locals局部變量槽數量,code_length標識機器字節碼長度,往後查詢字節碼如下圖所示,其實就是0/1/4/5/6/9的指令集。Code類型又嵌套異常屬性表、行號表LineNumberTable、LocaVariableTable 局部變量表等等信息。如下圖javap生成的類定義信息

1.Code1方法執行過程:

構造方法:descriptor ()V標識無參無返回值爲Void的方法索引,flags可見性修飾符;

程序運行時,先將常量池、方法字節碼、字符串常量池,靜態變量加載到元數據區(1.8後字符串常量池,靜態變量放入了堆);main線程開始運行,分配棧幀內存,其中操作數棧stack=2表示運行該方法所需要的最大操作數棧的深度是2;locals=1表示該運行方法所需要的最大局部方法表的最大slot數據是1;args_size是該方法的形參個數,如果是實例方法 第一個形參是this引用。此例正是this引用。所以args_size=1+實際的參數

aload_0: 加載 slot0的局部變量,即this,作爲下面的invokespecial 構造方法調用的參數

invokespecial: 調用構造方法,常量池第#1項,即【Method java/lang/Object."<init>":()V】

aload_0 :再次加載 slot0的局部變量,即this

iconst0: 將int類型爲0的數值壓入棧頂(爲什麼要再放入棧頂,我個人人爲可能是下面初始化實例會需要指定到當前的實例對象)

putfileld: 將常量池中#2 也就是com/jd/crm/Logback/TestClass.instanceVar 實例變量賦值爲0,並彈出棧。

通過以上指令操作,對象已經初始化,可發現在實例變量初始化之前是先調用的構造器方法,後才初始化實例變量。

1.Code2方法instanceMethod執行過程:

descriptor標識爲int類型入參、int類型出參

flags標識方法問public類型

statck=2代表棧深度爲2,locals=2標識預留兩個局部變量槽;args_size=2標識兩個參數,分別爲隱藏的this和方法的形式參數,下標[0]=this、 [1]=param 如下所示

LocalVariableTable:

Start Length Slot Name Signature

0 4 0 this Lcom/jd/crm/Logback/TestClass;

0 4 1 param I

0:iload_1 標識將上面局部變量槽LocalVariableTable下標爲1的param參數壓入棧

1:iconst_1 將int類型爲1的常量數字壓入棧

2: iadd 將當前棧頂的兩個元素 param和1相加

3: ireturn 返回

LineNumberTable:

line 10: 0

標識實際java源代碼的行數

2.10 字節碼指令簡介

•加載和存儲指令:

•運算指令

•類型轉換指令

•對象創建和訪問指令

•操作數棧管理指令

•控制轉移指令

•異常處理指令

•同步指令

•方法調用和返回執行

invokervirtual:調用對象的實例方法 invokerinterface 調用接口方法,自動運行期搜索一個實現接口的對象進行方法調用;invokerspeical:調用init、私有和父類調用的特殊方法調用;invokedynamic:運行時動態解析

3 類文件加載

3.1 加載

jvm通過classLoader(雙親委派)將class類文件二進制流加載到元數據區內存,

將字節流所標識的靜態存儲結構轉換爲元數據區的動態存儲

在堆內存創建一個Class對象,堆中的Class並不存儲靜態變量、常量、方法等實際信息(實際存儲元空間),可以看做只是一個句柄,通過對象頭的類指針指向元空間類信息。這樣在強制轉換或者InstanceOf判斷時,會根據對象中的類指針指向元空間的類常量池進行判斷是否爲同一個類。

3.2 驗證

1、文件格式驗證

2、元數據驗證

3、字節碼驗證

4、符號引用驗證

3.3 準備

準備階段是爲類變量(靜態變量)分配內存並設置類變量初始值的階段,分配這些內存是在元數據區裏面進行的,但是類變量(無final修飾的靜態變量)、字符串常量在1.8及以後都放入了堆區間。這個階段有兩點需要重點介紹以下的:

1、只有類變量(被static修飾的變量賦值初始值,static final修飾的賦值爲程序指定值)會分配內存,不包括實例變量,實例變量是在對象實例化的時候在堆中分配內存的。

2、設置類變量的初始值是數量類型對應的默認值,而不是代碼中設置的默認值。例如public static int number=111,這類變量number在準備階段之後的初始值是0而不是111。而給number賦值爲111是在類的初始化階段。

3.4 解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。

符號引用:常量池中類、字段的常量字符串表示方式

類和接口的解析舉例:假如類A引用了類B,加載階段是靜態解析,這時候B還沒有被放到JVM內存中,這時候A引用的只是代表B的符號,這是符號引用。

直接引用: 指向目標的指針或者相對偏移量

類和接口的解析舉例:類A在解析階段發現自己符號引用了B,如果這個時候B還沒被加載。就是直接觸發B的類加載,加載後會在運行常量池存儲B的有效類信息地址,並且直接引用。

•類和接口的解析

•字段解析根據常量池字段filedrf_info中的符號進行解析,首先在符號引用的類中根據簡單名稱和字段描述符查找,如果查到則返回這個字段的直接引用並結束,否則從下往上地櫃各個父類查找,如果還未查到則拋出NoSuckFieldError異常

•方法解析

•接口方法解析

4 類實例初始化

初始化,爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化clinit方法。在Java中對類變量進行初始值設定有兩種方式:定義靜態變量並指定值、使用靜態代碼塊

對象初始化

4.1 初始化對象前檢查

jvm碰到一個new指令,首先判斷改指令指向的常量池的類全名是否被加載、解析初始化過,如果沒有則進行類加載,參考類文件加載

4.2 內存分配

通過jvm內存分配機制,此分配機制取決回收機制,通過指針碰撞方法或者空閒列表方式進行堆內存分配;

1.指針碰撞法 假設Java堆中內存是完整的,已分配的內存和空閒內存分別在不同的一側,通過一個指針作爲分界點,需要分配內存時,僅僅需要把指針往空閒的一端移動與對象大小相等的距離。使用的GC收集器:Serial、ParNew,適用堆內存規整(即沒有內存碎片)的情況下。這兩種都是新生代垃圾收集器,因此都是使用複製算法,可以得到比較完整的內存區域。

2.空閒列表法 事實上,Java堆的內存並不是完整的,已分配的內存和空閒內存相互交錯,JVM通過維護一個列表,記錄可用的內存塊信息,當分配操作發生時,從列表中找到一個足夠大的內存塊分配給對象實例,並更新列表上的記錄。使用的GC收集器:CMS,適用堆內存不規整的情況下。從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種“標記-清除”算法實現的,因此會得到很多碎片因此和空閒列表配合使用。

內存分配併發問題

在創建對象的時候有一個很重要的問題,就是線程安全,因爲在實際開發過程中,創建對象是很頻繁的事情,作爲虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機採用兩種方式來保證線程安全:

•CAS: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操作的原子性。

•TLAB(本地現成緩衝區): 爲每一個線程預先分配一塊堆內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大於TLAB中的剩餘內存或TLAB的內存已用盡時,再採用上述的CAS進行內存分配。

4.3 初始化0值

內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

4.4 對象頭設置

初始化零值完成之後,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。這些信息存放在對象頭中。另外,根據虛擬機當前運行狀態的不同,如是否啓用偏向鎖等,對象頭會有不同的設置方式。

4.5 實例構造器初始化

4.6 對象的內存佈局

對象在對中的存儲佈局主要分爲三部分,對象頭、實例數據、對齊填充

對象頭:

主要兩類:其主要包括兩部分數據:Mark Word、Class對象指針。特別地對於數組對象而言,其還包括了數組長度數據。在64位的HotSpot虛擬機下,Mark Word佔8個字節,其記錄了Hash Code、GC信息、鎖信息等相關信息;而Class對象指針則指向該實例的Class對象。

HotSpot對象頭

實例數據:對象定義的實例變量,這部分數據存儲受到虛擬機分配策略參數(-XX:FieldsAllocationStype)和字段定義的順序影響。HotSpot默認分配的策略是將相同寬度字段一起存放,父類的變量會出現在子類變量之前。

對齊填充:jvm存儲任何大小必須是8個字節的整數倍,不夠補齊。這個和類二級制字節流一致。下面是個無鎖狀態的對象實例化後的數據結構,使用jol工具打印出的實例佈局如下

5 對象的訪問

5.1 句柄訪問

Java堆中將會劃分出一塊內存來作爲句柄池,reference中 存儲的就是對象

的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信 息

5.2 直接訪問

直接訪問是reference中直接存儲的實例對象的地址,實例對象中包含了類對象的訪問指針,也就是如果訪問類對象需要多一層引用

優缺點

這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而reference本身不需要修改。 使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷, 由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。就本書討論的主要虛擬機Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各種語言和框架使用句柄來訪問的情況也十分常見

6 虛擬機字節碼執行引擎

6.1 運行時棧幀結構

1.局部變量表:在class文件被編譯時,就已知某個方法的局部變量槽有幾個,主要存放方法參數和方法內部定義的局部變量

2.操作數棧:和局部變量表相似,編譯時就明確了操作數棧的深度

3.動態鏈接:大部分類在類加載解析過程中,會將符號引用轉爲直接引用,也就是在類加載階段清楚調用哪個類的哪個方法(這些方法調用參考字節碼指令簡介中invoke*指令),但是有一部分必須在運行期間才能確定目標的方法的直接引用。

4.方法返回地址

6.2 方法調用

1.解析:在內解析階段,會將符號引用轉換爲直接引用,這種在解析階段就能確定的調用方法版本稱爲解析,比如invokesatic invokespecial invokevirtual等等指令指示的方法調用

2.靜態分派:方法的重載,虛擬機需要根據方法的入參個數和類型方能定位到某個具體方法,發生在編譯階段,故也屬於一種解析方式

3.重載方法匹配優先級:方法重載過程中,涉及方法的入參和個數,而入參存在自動類型轉換,比如重載方法入參爲char類型,如果不存在入參爲char類型的方法匹配,則char進行自動類型轉換爲int類型,在最終匹配了Int入參類型的方法。方法重載的本質

4.動態分配:如下圖所示,man和women和重新man引用指向women然後方法調用sayHello,此時字節碼顯示的符號引用都是Human#sayHello,但是實際執行結果和指令碼不一致,這是因爲invokevirtual指令,在指令調用之前都會aload_x來加載實際的數據類型,這就是方法重寫的本質

5.invokedynamic指令:爲了解決其他invok*指令方法分配規則完全固化在虛擬機中的問題,jvm支持設計者更高的靈活度,將動態調用可以以api的方式直接使用。參考java.lang.invoke包的使用方式。

6.3 基於棧的字節碼解釋執行引擎

jvm是基於棧的指令集合,這種指令自身不帶參數,使用操作數棧的輸入輸出作爲指令本身的參數。物理機一般是基於寄存器的指令集,指令本身攜帶參數並存放在寄存器。

下面是一個基於棧來展示在虛擬機中字節碼是如何執行的。

以上字節碼執行過程如下

7 容易混淆點

7.1 文件常量池

類加載後,類的域字段、方法和類描述信息會加載到元數據區,既屬於類的靜態常量池

7.2 運行時常量池

我們上面說的class文件中的常量池,它會在類加載後進入方法區中的運行時常量池。並非只有Class定義的文件常量合併處理後放入運行時常量池,在運行期間也可以將新的常量放入池中,比如String類的intern方法

7.3 字符串常量池

字符串常量池存放在堆內存(>=1.8)中,堆裏邊的字符串常量池存放的是字符串的引用或者字符串(兩者都有),如下圖描述字符串創建的堆分佈

上圖說明:

引用初始化初始化s、s2是先看常量池,有就返回對象引用,否則創建abc對象,然後創建s1/s2Ref常量引用返回

字符串相加:先創建StringBuilder對象,然後apend字符串a、apend字符串b 然後toString(new方法)生成字符串ab對象並在字符串常量池生成引用返回,爲什麼不要字符串相加,就是因爲會生成大量StringBuilder對象

String s = "a"+"b";//返回的是常量池的ab字符串的引用
String s1 ="ab";
System.out.println(s == s1);//因兩個最終都指向字符串常量池,所以爲true

new 字符串相當於堆創建兩個對象,一個String對象,然後創建字符串堆存儲,然後String對象引用到字符串的堆存儲,

String s1 ="a";
String s = new String ("a").intern();//強制生成字符串常量池引用
System.out.println(s == s1);//返回true
String s1 ="a";
String s = new String ("a");
System.out.println(s == s1);//返回false

8 附件

jvm常量池類型和結構體定義

常量池類型

常量池類型結構定義

常見的屬性類型

jdk版本好class字節版本號對應關係

屬性表類型

作者:京東物流 王北永

來源:京東雲開發者社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章