java類加載之連接過程(附hotspot類對象描述)

前一篇文章,介紹了字節碼是如何被加載,本文介紹一下加載流程中的連接過程,先從內存存儲結構提及。java

這裏以hotspot爲例,在讀取class文件以後,會建立一個jvm層面(C++)的描述java類的InstanceClass對象:c++

// InstanceKlass layout:
// [C++ vtbl pointer ] Klass
// [subtype cache ] Klass
// [instance size ] Klass
// [java mirror ] Klass
// [super ] Klass
// [access_flags ] Klass
// [name ] Klass
// [first subklass ] Klass
// [next sibling ] Klass
// [array klasses ]
// [methods ]
// [local interfaces ]
// [transitive interfaces ]
// [fields ]
// [constants ]
// [class loader ]
// [source file name ]
// [inner classes ]
// [static field size ]
// [nonstatic field size ]
// [static oop fields size ]
// [nonstatic oop maps size ]
// [has finalize method ]
// [deoptimization mark bit ]
// [initialization state ]
// [initializing thread ]
// [Java vtable length ]
// [oop map cache (stack maps) ]
// [EMBEDDED Java vtable ] size in words = vtable_len
// [EMBEDDED nonstatic oop-map blocks] size in words = nonstatic_oop_map_size
// The embedded nonstatic oop-map blocks are short pairs (offset, length)
// indicating where oops are located in instances of this klass.
// [EMBEDDED implementor of the interface] only exist for interface
// [EMBEDDED host klass ] only exist for an anonymous class (JSR 292 enabled)
複製代碼

能夠看到這個C++類描述了一個class file的類結構(如java.lang.Integer),根據它就能夠建立java類對應的實例了。hotspot中用instanceOopDesc描述一個java類實例對象,其結構包括對象標記,指向InstanceClass的指針,對象實例數據。數組

  • _java_mirror: InstanceMirrorKlass類型(InstanceKlass的子類),是一個類結構在java語言層面的描述,即java.lang.Class類型對象的引用。
  • _constants: 常量池(ConstantPool)指針,初始時常量池中仍是符號引用,解析事後會緩存
  • _local_interfaces_transitive_interfaces爲直接實現和繼承的接口列表
  • _methods: 方法列表
  • _fields:字段列表索引,項爲六元組[access, name index, sig index, initval index, low_offset, high_offset]

數組是類似的結構,這裏不提了緩存

另外,在InstanceKlass定義了類的狀態:jvm

state desc
allocated 已分配,還未連接
loaded 已加載並插入類體系,還未連接
linked 已成功連接,但還未初始化
being_initialized 正在初始化
fully_initialized 已成功初始化(這時成功時的最終狀態)
initialization_error 初始化失敗

接下來,就是本文要介紹的jvm關於連接過程的規範。oop

​ 前文提到了連接包含驗證 -> 準備 -> 解析三個階段,jvm規範裏並無嚴格限制實現者如何實現,因此不一樣的jvm實現能夠不一樣,實際上不少虛擬機在爲類結構分配內存前已經完成了部分驗證工做,解析則可能在初始化時才進行,並且一個類的一個階段(好比驗證是否繼承了final修飾的類)會激活另外一個類的加載過程,實際狀況會很複雜。this

驗證

驗證工做不是一次完成,而是分爲多步的,主要驗證的內容有字節碼靜態結構,語法和指令執行。spa

  • 結構驗證3d

    • 前4個字節必須是魔數(0xcafebabe)
    • 版本是否支持
    • 全部可識別的屬性都能正確的解析
    • class文件末尾不能有多餘字節
    • 常量池是有效的(引用都能指向有效的常量位置且是正確的引用)
    • 字段和方法的引用name_index,descriptor_index都是有效的常量池常量(注意:此時並不驗證引用類)
  • 語法驗證指針

    語法驗證包括類繼承是否合法,方法重載,覆蓋是否正確,非抽象類是否存在抽象方法等等,保證語法正確性。這個過程固然還涉及到常量池裏引用的常量是否有效,訪問範圍合法性等。

  • 字節碼指令驗證

    • 指令靜態結構驗證:主要驗證code屬性中指令數組的指令是否能正確識別並被當前虛擬機支持,指令及對應操做數是否匹配,數組以指令開始,操做數若是指向常量池必須是有效引用,操做數若是表示局部變量表索引則不能超過max_locals - 1(long,double相關指令最大max_locals - 2)等靜態約束

    • 動態指令執行分析驗證:靜態驗證只能驗證指令流的語法正確性,但不能保證其執行的語義有效性,例如,iload指令去讀取一個對象引用,jsr跳到方法外去了,return了一個不匹配的類型值等。因此還須要從執行分析。

      執行分析驗證有早期的類型推導(Type Inference)和jdk1.6以後的基於類型檢查(Type Checking,經過StackMapTable)的驗證,這塊內容比較複雜,我也沒研究,因此就不寫了

Note: 一個類或接口的驗證若是觸發另外一個類或接口M被加載,但不要求M執行驗證與準備

準備

準備過程就是建立類或接口的靜態field併爲它分配零值,注意不是執行類構造器賦值(在初始化階段執行)。對於基本類型零值就是0,對於引用類型零值爲null

能夠這樣理解:源碼中靜態字段賦值都是編譯的時候轉換成方法中的賦值指令,因此要等到指令執行後才賦值爲真正的值,但虛擬機在給類字段分配內存時須要先給它個值,因此給了零值。

jvm規範沒有要求準備階段確切何時執行但必定要在初始化執行前。

解析

解析過程就是將運行時常量池中的符號引用轉換爲直接引用的過程

  • 符號引用

    符號引用就是以前講字節碼時說的以字面量形式表示的引用,如Ljava.lang.Object,這種引用方式只能表示引用的是什麼,但不知道引用的對象在哪裏(內存中的位置)

  • 直接引用

    直接引用就是表示的被引用對象在內存中分配後的地址

遇到anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, ldc2_w, multianewarray, new, putfield, putstatic指令時須要進行解析,已解析過的常量會被標識以防止重複解析,但invokedynamic指令每次執行都會解析。

解析的對象有類或藉口字段類方法接口方法方法句柄動態調用點

  • 類和接口解析
  • 字段解析
  • 方法解析
  • 接口方法解析
  • 方法類型和方法句柄解析
  • 動態調用點解析

具體解析過程,在不一樣的jdk版本中有所區別,沒有精力寫了,感興趣能夠去看看官方文檔:D

往期內容:

詳解字節碼(class)文件

讀取class文件

經過源碼,實例詳解java類加載機制

下期預告:

jvm類初始化詳解

歡迎關注!

相關文章
相關標籤/搜索