深入理解HotSpotVM中的oop模型(Klass/Class/Oop)

本文主要詳細介紹Klass/Class/Oop之間的關系,從HotSpotVM的Cpp源碼層面去進行相關的方法解析,還會介紹方法區的動態性的原理(運行時動態生成和加載字節碼的原理)。

ps:本文中的環境使用的是OpenJDK11

1. 一起去了解HotSpotVM中的Oop模型吧!

我們以下面的代碼(Main.java),去進行舉例,從而去完整介紹整個Java的對象模型。

public class Main {
    public static void main(String[] args) {
        Object obj = new Object();
    }
}

1.1 編譯java代碼成為字節碼文件(Main.class)

使用javac編譯器去對Main.java去進行編譯,生成一個Main.class這樣一個字節碼文件。

1.2 HotSpotVM進行類加載

HotSpotVM啟動時,使用類加載子系統對Main類以及java類庫中的一些類去進行加載。在加載時,會根據.class字節碼文件中類信息保存到instanceKlass對象中(我們一般可以稱之為類模板對象)、常量池信息保存到運行時常量池中,這些信息都會被存放到方法區(jdk1.8之后的版本的JDK的實現稱為元空間,之前的版本稱之為永久代,下面使用方法區去進行描述)中。

實際上,針對于每個字節碼文件中的類,它還會生成一個Class(java/lang/Class)對象放到堆空間中,并且instanceKlass對象中還有一個字段java_mirror指向了堆中的Class對象。

HotSpotVM并不直接將instanceKlass對象暴露給java代碼中去進行使用,而是暴露了一個mirror(鏡像)給java程序去進行使用,這個mirror也就是java/lang/Class對象(本質上也是個oopDesc,但是它比較特殊,下面都使用"Class對象"去進行描述)。

Class對象,是不是很熟悉?其實就是我們反射中經常使用到的那個Class對象呀,HotSpotVM提供了一個Class對象給我們去訪問方法區中的內容!我們在java代碼層面,可以通過類似如下的幾種方式去獲取到Class對象。

        Object obj = new Object();
        final Class<?> cl1 = Class.forName("java.lang.Object");
        final Class<?> cl2 = Object.class;
        final Class<?> cl3 = obj.getClass();

1.3 java對象的JOL內存布局

當我們使用Object obj = new Object()這樣的java代碼去創建一個Object類型的對象時,會在堆中分配一個Object對象空間,并且在JVM虛擬機棧(簡稱VM棧)中創建一個引用obj,分配到棧幀的局部變量表(Local Variables)中的某個槽位中。

而一個java對象在堆中存儲的內存布局,稱為JOL(Java Object Layout),存儲的方式也就是下面這樣的一個圖

image.png

一個java對象主要組成部分:

  • 1.markword,在32bitVM中是4個字節,64bitVM中是8個字節,現在都是64bit虛擬機多,一般都是8個字節。
  • 2.類型指針,其實應該是klass指針,它就是指向了方法區中的instanceKlass對象。有4字節/8字節兩種,取決于開不開啟壓縮,現在一般的VM默認都是開啟壓縮的,也就是一般都是4個字節。
  • 3.數組長度,占用4個字節,我們也可以知道java中數組的最大長度只有Integer.MAX_VALUE,底層規定死的只有4個字節,只有數組才有該組成部分。
  • 4.實例數據,主要存放的就是對象的成員變量。
  • 5.padding,主要用來做4/8字節填充,比如之前幾個部分的長度只有22個字節,就得填充成為4/8的整數倍,需要使用padding去填充兩個字節。(4字節填充還是8字節填充,取決于操作系統,以及使用的編譯器以及配置的相關參數,默認32bit系統是4字節填充,64bit系統下是8字節填充,當然也可以通過參數去進行修改,下面會有提到)

Java對象的存儲,其實就是采用如下的這樣一個Cpp層面的oopDesc對象去進行存儲的(一個java對象對應一個oopDesc),在HotSpotVM中的實現也就是下面這樣的一個類oopDesc(src/hotspot/share/oops/oop.hpp):

class oopDesc {
  friend class VMStructs;
  friend class JVMCIVMStructs;
 private:
  volatile markOop _mark;  //markOop,也就是markword對象指針
  union _metadata {  //元信息,聯合體,Klass指針或者是壓縮指針,Klass也就是指向方法區中的類的元信息的對象
    Klass*      _klass;   //Klass為Klass類,可以開啟非壓縮指針,這樣就占用8B去存放
    narrowKlass _compressed_klass;   //使用uint去存放...默認情況下都會使用壓縮指針去存放以節省空間,只占有4B
  } _metadata;
}

而OopDesc的子類有instanceOopDescarrayOopDesc這兩個,它們分別用來描述普通類型的對象、數組類型的對象的對象指針。

1.4 從HotSpotVM源碼中去對instanceOopDesc去進行解析

我們來看instanceOopDesc這個子類的實現

class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

1.4.1 對C語言中的對齊填充原理的介紹

這里我需要對C語言中對齊填充的概念進行說明一下,在32位系統下是采用4字節填充,64位系統下是8字節填充。

#include "stdio.h"
struct data
{
    long data;
    int data0;
};
int main()
{
    printf("%lu", sizeof(struct data));
}

比如上面的C語言代碼在32位系統下的輸出為12,在64位系統下的輸出為16,就是因為它們的對齊方式不同,64位系統下long的長度+int的長度=12,不足8的倍數需要填充4個字節湊夠8的倍數。我們有什么辦法去修改多少字節對齊嗎?當然是有的。看下面的代碼

#include "stdio.h"
#pragma pack(4)
struct data
{
    long data;
    int data0;
};
int main()
{
    printf("%lu", sizeof(struct data));
}

使用上面的代碼中的#pragma pack(4),就將其改為了4字節對齊,輸出的結果就是12,而不是之前的16。關于padding,其實還有很多說的,比如你們可以猜猜下面的結構體使用8字節對齊占用多少個字節?(答案是32字節,不感興趣的跳過即可,感興趣的可以看看解析或者自己試試?)

struct p
{
    char c;
    int d;
    short i;
    double f;
    char g;
};

簡單解析一下吧(可能不太直觀,讀者可以自行畫圖理解,這里畫圖太麻煩,暫時忽略掉它吧):

首先需要進行說明的是:在64bit系統下,char類型的變量需要滿足地址能被1整除,int類型的變量需要滿足地址能被4整除,short類型的變量需要滿足地址能被2整除,double/long類型的變量需要滿足地址能被8整除;最終整個結構體的占用的空間要能被8整除。

我們把內存看做是一個大的byte[],這樣變量c(char)被放到了byte[0]處,但是變量d(int)需要內存地址能被4整除,因此放到了byte[4..7],這樣byte[1..3]就變成了padding,變量i(short)需要能被2整除,因此被放到了byte[8..9]上,變量f(double)需要地址能被8整除,因此它會被放到byte[16..23]上,byte[10..15]就變成了padding,變量g(char)被放到了byte[24]上,到這里已經結束了,你是不是以為就只占用25B?上面有說這樣一條原則,就是整個結構體占用的空間要能被8整除,也就是做8字節填充,因此byte[25..31]都成為了padding,最終占用的空間就是32B。

1.4.2 對HotSpotVM中HeapWord和HeapWordSize的說明

在HotSpotVM中,將內存的地址的訪問抽象成為了HeapWord,我們通過源碼查看HeapWord的定義

class HeapWord {
  friend class VMStructs;
 private:
  char* i;
 public:
  char* value() { return i; }
};

其實就是包裝了一個char*的指針,代表的是一個內存地址。VM提供了相應的方法申請VM內存,這些方法分配內存之后返回的都是一個HeapWord*對象,表示的是VM中的內存地址

而HeapWordSize呢?它是做什么的?我們找到它的定義

const int HeapWordSize        = sizeof(HeapWord);

我們發現它其實就是計算HeapWord占用的空間大小,計算來干什么?其實它計算的就是當前平臺下的指針變量占用多少個字節char*其實就是一個普通的指針變量,在32位系統下指針變量占用的長度也就是4個字節,在64位系統下占用的長度也就是8個字節。也就是說HeapWordSize在32位系統下為4,64位系統下為8

1.4.3 進一步了解instanceOopDesc中的相關方法

在完成了對字節填充的概念之后,你應該能更好地理解對象的oop模型,下面我們可以看來instanceOopDesc類提供的相關方法。

比如instanceOopDesc中的下面這個header_size方法:

static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

我們事先說明:下面的所有介紹,都是在現在普遍的64位系統的情況下

我們根據剛剛的字節填充的概念,我們知道sizeof(instanceOopDesc)在8字節對齊的情況下為16(不管開沒開啟壓縮指針),HeapWordSize為8,因此計算出來的header_size也就是2。

下面來看base_offset_in_bytes方法

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }

我們可以看到,它其實就是判斷是否進行過Klass指針壓縮,從而進行不同的判斷,因為如果進行過Klass指針的壓縮,對象的字段(Field)就可能出現不是8字節對齊的情況,只占用了12B,因為8字節填充那么就有4字節會被白白浪費掉,壓縮指針不是白壓縮了嗎?它的作用就是需要將浪費的4B字節都利用上來

如果沒開啟壓縮指針,直接返回sizeof(instanceOopDesc),也就是16作為base_offset。如果開啟了壓縮指針,那么就需要計算出來markword的長度+壓縮指針的長度,也就是12作為base_offset。那么我們可以猜到base_offset是做什么的?其實就是計算出來對象的Field的基地址相對于oop的基地址的偏移量,也就oop+base_offset=對象Field的基地址

再比如它的contains_field_offset方法,用來判斷指定的offset是否包含在對象的字段所在的包含區域內,也是利用到了之前提到的base_offset_in_bytes方法。

  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }

從instanceOopDesc這個類中其實我們并沒有在C++代碼中看到它有存放該對象的Field,而是直接將其放到instanceOopDesc對象之后緊挨著,然后通過base_offset_in_bytes計算出來Field的基礎偏移量,然后找到Field所處的基地址,這才是C/Cpp中指針的奧義,指針可以指萬物!它不直接進行定義最終的目的嘛,其實就是為了更好利用內存罷了。

1.5 從HotSpotVM源碼中去對arrayOopDesc去進行解析

class arrayOopDesc : public oopDesc {
  friend class VMStructs;
  friend class arrayOopDescTest;

  // Interpreter/Compiler offsets

  // Header size computation.
  // The header is considered the oop part of this type plus the length.
  // Returns the aligned header_size_in_bytes.  This is not equivalent to
  // sizeof(arrayOopDesc) which should not appear in the code.
  static int header_size_in_bytes() {
    size_t hs = align_up(length_offset_in_bytes() + sizeof(int),
                              HeapWordSize);

  // Check whether an element of a typeArrayOop with the given type must be
  // aligned 0 mod 8.  The typeArrayOop itself must be aligned at least this
  // strongly.
  static bool element_type_should_be_aligned(BasicType type) {
    return type == T_DOUBLE || type == T_LONG;
  }

 public:
  // The _length field is not declared in C++.  It is allocated after the
  // declared nonstatic fields in arrayOopDesc if not compressed, otherwise
  // it occupies the second half of the _klass field in oopDesc.
  static int length_offset_in_bytes() {
    return UseCompressedClassPointers ? klass_gap_offset_in_bytes() :
                               sizeof(arrayOopDesc);
  }

  // Returns the offset of the first element.
  static int base_offset_in_bytes(BasicType type) {
    return header_size(type) * HeapWordSize;
  }

  // Returns the address of the first element. The elements in the array will not
  // relocate from this address until a subsequent thread transition.
  inline void* base(BasicType type) const;
  inline void* base_raw(BasicType type) const; // GC barrier invariant

  template <typename T>
  static T* obj_offset_to_raw(arrayOop obj, size_t offset_in_bytes, T* raw) {
    if (obj != NULL) {
      assert(raw == NULL, "either raw or in-heap");
      char* base = reinterpret_cast<char*>((void*) obj);
      raw = reinterpret_cast<T*>(base + offset_in_bytes);
    } else {
      assert(raw != NULL, "either raw or in-heap");
    }
    return raw;
  }

  // Tells whether index is within bounds.
  bool is_within_bounds(int index) const        { return 0 <= index && index < length(); }

  // Accessors for instance variable which is not a C++ declared nonstatic
  // field.
  int length() const {
    return *(int*)(((intptr_t)this) + length_offset_in_bytes());
  }
  void set_length(int length) {
    set_length((HeapWord*)this, length);
  }
  static void set_length(HeapWord* mem, int length) {
    *(int*)(((char*)mem) + length_offset_in_bytes()) = length;
  }

  // Should only be called with constants as argument
  // (will not constant fold otherwise)
  // Returns the header size in words aligned to the requirements of the
  // array object type.
  static int header_size(BasicType type) {
    size_t typesize_in_bytes = header_size_in_bytes();
    return (int)(element_type_should_be_aligned(type)
      ? align_object_offset(typesize_in_bytes/HeapWordSize)
      : typesize_in_bytes/HeapWordSize);
  }

  // Return the maximum length of an array of BasicType.  The length can passed
  // to typeArrayOop::object_size(scale, length, header_size) without causing an
  // overflow. We also need to make sure that this will not overflow a size_t on
  // 32 bit platforms when we convert it to a byte size.
  static int32_t max_array_length(BasicType type) {
    assert(type >= 0 && type < T_CONFLICT, "wrong type");
    assert(type2aelembytes(type) != 0, "wrong type");

    const size_t max_element_words_per_size_t =
      align_down((SIZE_MAX/HeapWordSize - header_size(type)), MinObjAlignment);
    const size_t max_elements_per_size_t =
      HeapWordSize * max_element_words_per_size_t / type2aelembytes(type);
    if ((size_t)max_jint < max_elements_per_size_t) {
      // It should be ok to return max_jint here, but parts of the code
      // (CollectedHeap, Klass::oop_oop_iterate(), and more) uses an int for
      // passing around the size (in words) of an object. So, we need to avoid
      // overflowing an int when we add the header. See CRs 4718400 and 7110613.
      return align_down(max_jint - header_size(type), MinObjAlignment);
    }
    return (int32_t)max_elements_per_size_t;
  }

};

其實主要內容和instanceOopDesc很類似的,只不過對象是存儲在16B(markword+klass+length)之后那段了而壓縮指針剩余的4字節用來去存放數組的length罷了(在源碼的注釋也給我們解釋了,length字段并沒有在C++代碼中去進行定義,因為有可能它會利用klass指針進行壓縮而剩下的一半空間,如果去進行定義了,那么該部分就利用不上了)。

1.6 對Klass的源碼去進行解析

Klass這個類中其實包括蠻多東西的,我們只研究一些重點的字段。(不重要的字段暫時忽略)

class Klass : public Metadata {
  friend class VMStructs;
  friend class JVMCIVMStructs;
   protected:
  // If you add a new field that points to any metaspace object, you
  // must add this field to Klass::metaspace_pointers_do().

  // note: put frequently-used fields together at start of klass structure
  // for better cache behavior (may not make much of a difference but sure won't hurt)
  enum { _primary_super_limit = 8 };

  // The "layout helper" is a combined descriptor of object layout.
  // For klasses which are neither instance nor array, the value is zero.
  //
  // For instances, layout helper is a positive number, the instance size.
  // This size is already passed through align_object_size and scaled to bytes.
  // The low order bit is set if instances of this class cannot be
  // allocated using the fastpath.
  //
  // For arrays, layout helper is a negative number, containing four
  // distinct bytes, as follows:
  //    MSB:[tag, hsz, ebt, log2(esz)]:LSB
  // where:
  //    tag is 0x80 if the elements are oops, 0xC0 if non-oops
  //    hsz is array header size in bytes (i.e., offset of first element)
  //    ebt is the BasicType of the elements
  //    esz is the element size in bytes
  // This packed word is arranged so as to be quickly unpacked by the
  // various fast paths that use the various subfields.
  //
  // The esz bits can be used directly by a SLL instruction, without masking.
  //
  // Note that the array-kind tag looks like 0x00 for instance klasses,
  // since their length in bytes is always less than 24Mb.
  //
  // Final note:  This comes first, immediately after C++ vtable,
  // because it is frequently queried.
  jint        _layout_helper;

  // Klass identifier used to implement devirtualized oop closure dispatching.
  const KlassID _id;  // Klass的唯一ID標識符

  // The fields _super_check_offset, _secondary_super_cache, _secondary_supers
  // and _primary_supers all help make fast subtype checks.  See big discussion
  // in doc/server_compiler/checktype.txt
  //
  // Where to look to observe a supertype (it is &_secondary_super_cache for
  // secondary supers, else is &_primary_supers[depth()].
  juint       _super_check_offset;

  // Class name.  Instance classes: java/lang/String, etc.  Array classes: [I,
  // [Ljava/lang/String;, etc.  Set to zero for all other kinds of classes.
  Symbol*     _name;  // 實例的類名的名稱

  // Cache of last observed secondary supertype
  Klass*      _secondary_super_cache;  // 存放上一次使用到的次超類的類型緩存
  // Array of all secondary supertypes
  Array<Klass*>* _secondary_supers;  // 所有的次超類的數組
  // Ordered list of all primary supertypes
  Klass*      _primary_supers[_primary_super_limit];  // 所有有序超類列表
  // java/lang/Class instance mirroring this class
  OopHandle _java_mirror;  // java/lang/Class的實例鏡像指針
  // Superclass
  Klass*      _super;  // 當前類的直接父類
  // First subclass (NULL if none); _subklass->next_sibling() is next one
  Klass*      _subklass;  // 當前類的第一個子類,
  // Sibling link (or NULL); links all subklasses of a klass
  Klass*      _next_sibling; // 鏈接所有的子類

  // All klasses loaded by a class loader are chained through these links
  Klass*      _next_link;  // 被同一個類加載器加載進來的類連成的鏈表

  // The VM's representation of the ClassLoader used to load this class.
  // Provide access the corresponding instance java.lang.ClassLoader.
  ClassLoaderData* _class_loader_data;  // 存放了加載當前這個類的類加載器數據

  jint        _modifier_flags;  // Processed access flags, for use by Class.getModifiers.
  AccessFlags _access_flags;    // Access flags. The class/interface distinction is stored here.

  JFR_ONLY(DEFINE_TRACE_ID_FIELD;)

  // Biased locking implementation and statistics
  // (the 64-bit chunk goes first, to avoid some fragmentation)
  jlong    _last_biased_lock_bulk_revocation_time;
  markOop  _prototype_header;   // Used when biased locking is both enabled and disabled for this type,原始的markword
  jint     _biased_lock_revocation_count;
}

存放了當前類的子類、父類、類加載器鏡像(java/lang/Class)的指針、訪問修飾符、原型markword等信息。還有一個我們可以關注的點,它繼承了Metadata類。

我們來研究它的子類instanceKlass?(字段太多了,下面暫時忽略部分,這里的話僅僅用來做相關的說明)

class InstanceKlass: public Klass {
  friend class VMStructs;
  friend class JVMCIVMStructs;
  friend class ClassFileParser;
  friend class CompileReplay;

  // See "The Java Virtual Machine Specification" section 2.16.2-5 for a detailed description
  // of the class loading & initialization procedure, and the use of the states.
  enum ClassState {
    allocated,                          // allocated (but not yet linked)
    loaded,                             // loaded and inserted in class hierarchy (but not linked yet)
    linked,                             // successfully linked/verified (but not initialized yet)
    being_initialized,                  // currently running class initializer
    fully_initialized,                  // initialized (successfull final state)
    initialization_error                // error happened during initialization
  };

 protected:
  // If you add a new field that points to any metaspace object, you
  // must add this field to InstanceKlass::metaspace_pointers_do().

  // Annotations for this class
  Annotations*    _annotations;  // 保存的是這個類上的注解列表
  // Package this class is defined in
  PackageEntry*   _package_entry; // 保存的是這個類所在的包名
  // Array classes holding elements of this class.
  Klass* volatile _array_klasses;  // 保存的是用來支持這個類的數組類的Klass
  // Constant pool for this class.
  ConstantPool* _constants;  // 當前這個類的常量池
  // The InnerClasses attribute and EnclosingMethod attribute. The
  // _inner_classes is an array of shorts. If the class has InnerClasses
  // attribute, then the _inner_classes array begins with 4-tuples of shorts
  // [inner_class_info_index, outer_class_info_index,
  // inner_name_index, inner_class_access_flags] for the InnerClasses
  // attribute. If the EnclosingMethod attribute exists, it occupies the
  // last two shorts [class_index, method_index] of the array. If only
  // the InnerClasses attribute exists, the _inner_classes array length is
  // number_of_inner_classes * 4. If the class has both InnerClasses
  // and EnclosingMethod attributes the _inner_classes array length is
  // number_of_inner_classes * 4 + enclosing_method_attribute_size.
  Array<jushort>* _inner_classes;

  // The NestMembers attribute. An array of shorts, where each is a
  // class info index for the class that is a nest member. This data
  // has not been validated.
  Array<jushort>* _nest_members;

  // The NestHost attribute. The class info index for the class
  // that is the nest-host of this class. This data has not been validated.
  jushort _nest_host_index;

  // Resolved nest-host klass: either true nest-host or self if we are not nested.
  // By always being set it makes nest-member access checks simpler.
  InstanceKlass* _nest_host;

  // the source debug extension for this klass, NULL if not specified.
  // Specified as UTF-8 string without terminating zero byte in the classfile,
  // it is stored in the instanceklass as a NULL-terminated UTF-8 string
  const char*     _source_debug_extension;
  // Array name derived from this class which needs unreferencing
  // if this class is unloaded.
  Symbol*         _array_name;

  // Number of heapOopSize words used by non-static fields in this klass
  // (including inherited fields but after header_size()).
  int             _nonstatic_field_size;  // 非static字段,包括在header_size之后的繼承的父類的字段

  // static字段的數量,包括這個klass中的oop和非oop
  int             _static_field_size;    // number words used by static fields (oop and non-oop) in this klass
  // Constant pool index to the utf8 entry of the Generic signature,
  // or 0 if none.
  u2              _generic_signature_index;  // 泛型簽名的索引,是一個指向常量池的索引
  // Constant pool index to the utf8 entry for the name of source file
  // containing this klass, 0 if not specified.
  u2              _source_file_name_index;  // 源文件的名字,是一個指向常量池的索引

  // static字段的數量,只包括這個klass中的oop字段
  u2              _static_oop_field_count;// number of static oop fields in this klass

  // java對象的字段的數量
  u2              _java_fields_count;    // The number of declared Java fields
  int             _nonstatic_oop_map_size;// size in words of nonstatic oop map blocks

  int             _itable_len;           // length of Java itable (in words)
  // _is_marked_dependent can be set concurrently, thus cannot be part of the
  // _misc_flags.
  bool            _is_marked_dependent;  // used for marking during flushing and deoptimization
  bool            _is_being_redefined;   // used for locking redefinition

  // The low two bits of _misc_flags contains the kind field.
  // This can be used to quickly discriminate among the four kinds of
  // InstanceKlass.

  static const unsigned _misc_kind_field_size = 2;
  static const unsigned _misc_kind_field_pos  = 0;
  static const unsigned _misc_kind_field_mask = (1u << _misc_kind_field_size) - 1u;

  static const unsigned _misc_kind_other        = 0; // concrete InstanceKlass
  static const unsigned _misc_kind_reference    = 1; // InstanceRefKlass
  static const unsigned _misc_kind_class_loader = 2; // InstanceClassLoaderKlass
  static const unsigned _misc_kind_mirror       = 3; // InstanceMirrorKlass

  // Start after _misc_kind field.
  enum {
    _misc_rewritten                           = 1 << 2,  // methods rewritten.
    _misc_has_nonstatic_fields                = 1 << 3,  // for sizing with UseCompressedOops
    _misc_should_verify_class                 = 1 << 4,  // allow caching of preverification
    _misc_is_anonymous                        = 1 << 5,  // has embedded _host_klass field
    _misc_is_contended                        = 1 << 6,  // marked with contended annotation
    _misc_has_nonstatic_concrete_methods      = 1 << 7,  // class/superclass/implemented interfaces has non-static, concrete methods
    _misc_declares_nonstatic_concrete_methods = 1 << 8,  // directly declares non-static, concrete methods
    _misc_has_been_redefined                  = 1 << 9,  // class has been redefined
    _misc_has_passed_fingerprint_check        = 1 << 10, // when this class was loaded, the fingerprint computed from its
                                                         // code source was found to be matching the value recorded by AOT.
    _misc_is_scratch_class                    = 1 << 11, // class is the redefined scratch class
    _misc_is_shared_boot_class                = 1 << 12, // defining class loader is boot class loader
    _misc_is_shared_platform_class            = 1 << 13, // defining class loader is platform class loader
    _misc_is_shared_app_class                 = 1 << 14, // defining class loader is app class loader
    _misc_has_resolved_methods                = 1 << 15  // resolved methods table entries added for this class
  };

  u2              _misc_flags;
  u2              _minor_version;        // minor version number of class file
  u2              _major_version;        // major version number of class file
  Thread*         _init_thread;          // Pointer to current thread doing initialization (to handle recusive initialization)
  OopMapCache*    volatile _oop_map_cache;   // OopMapCache for all methods in the klass (allocated lazily)
  JNIid*          _jni_ids;              // First JNI identifier for static fields in this class
  jmethodID*      volatile _methods_jmethod_ids;  // jmethodIDs corresponding to method_idnum, or NULL if none
  intptr_t        _dep_context;          // packed DependencyContext structure
  nmethod*        _osr_nmethods_head;    // Head of list of on-stack replacement nmethods for this class

  volatile u2     _idnum_allocated_count;         // JNI/JVMTI: increments with the addition of methods, old ids don't change

  // Class states are defined as ClassState (see above).
  // Place the _init_state here to utilize the unused 2-byte after
  // _idnum_allocated_count.
  u1              _init_state;                    // state of class,類的加載狀態,是一個枚舉類型,定義在上面了
  u1              _reference_type;                // reference type,引用類型(強軟弱虛)

  u2              _this_class_index;              // constant pool entry,指向常量池中class的索引

  // Method array.
  Array<Method*>* _methods;  // klass的方法數組
  // Default Method Array, concrete methods inherited from interfaces
  Array<Method*>* _default_methods;  // 繼承自接口的default方法數組
  // Interface (Klass*s) this class declares locally to implement.
  Array<Klass*>* _local_interfaces;  // 本地實現的接口
  // Interface (Klass*s) this class implements transitively.
  Array<Klass*>* _transitive_interfaces;  // 這個類實現的過渡接口
  // Int array containing the original order of method in the class file (for JVMTI).
  Array<int>*     _method_ordering;
  // Int array containing the vtable_indices for default_methods
  // offset matches _default_methods offset
  Array<int>*     _default_vtable_indices;

  // Instance and static variable information, starts with 6-tuples of shorts
  // [access, name index, sig index, initval index, low_offset, high_offset]
  // for all fields, followed by the generic signature data at the end of
  // the array. Only fields with generic signature attributes have the generic
  // signature data set in the array. The fields array looks like following:
  //
  // f1: [access, name index, sig index, initial value index, low_offset, high_offset]
  // f2: [access, name index, sig index, initial value index, low_offset, high_offset]
  //      ...
  // fn: [access, name index, sig index, initial value index, low_offset, high_offset]
  //     [generic signature index]
  //     [generic signature index]
  //     ...
  Array<u2>*      _fields; 
}

在instanceKlass中存放了類的注解信息、包名、常量池信息、內部類數組、內部成員、方法數組、字段數組等相關信息。

還有一個字段是值得我們去關注的,那就是_init_state,它定義為一個枚舉類型ClassState,它記錄的是當前類的加載狀態,也就是用來說明我們的當前類究竟處于哪個階段了?這就回到了類加載的那幾個階段:類的加載(Loading)、鏈接(Linking)、初始化(Initialization)。在一個類加載到HotSpotVM中,并不會立刻完成三個步驟,初始化環節需要在一個類被主動使用時才會執行。

  enum ClassState {
    allocated,  // 已分配
    loaded,  // 已加載,并添加到類的繼承體系中
    linked,  // 鏈接/驗證完成
    being_initialized,  // 正在初始化
    fully_initialized,  // 初始化完成
    initialization_error  // 初始化失敗
}
  • 1.類的加載(Loading)階段,也就是從磁盤/網絡/jar包等地方獲取字節碼文件。
  • 2.類的鏈接(Linking)階段,主要包括:驗證、準備、解析三個步驟。
    • 2.1 驗證主要是對字節碼文件去進行驗證,判斷字節碼是否符合當前的VM的要求。
    • 2.2 準備階段主要給類變量(static變量)賦給0值(8種基礎類型就是0,對象類型就是null),以及給一些常量去進行賦真實值(不需要new的常量,比如public static final String str = "wanna")。
    • 2.3 解析階段主要是將常量池中的符號引用轉換成為直接引用。(ps:其實這步應該是在初始化完成之后再執行)
  • 3.初始化(Initialization)階段主要是執行類的構造器方法(<clinit>),<clinit>是javac在編譯時收集所有的類變量的賦值操作和靜態代碼塊整合完成的一個方法。(需要new的常量就會在這里完成,比如public static final String str = new String("wanna")public static final Object obj = new Object()會在這里完成賦值)

我們可以發現其實instanceKlass就是HotSpotVM的類加載器在進行類加載時,每個字節碼文件就會映射并生成這樣的一個instanceKlass對象放到方法區中,而instanceKlass中就存放了字節碼文件中的相關信息。我們在字節碼文件中看到的常量池,會被HotSpotVM進行加載,放入運行時常量池(ConstantPool)中,在instanceKlass中也存放了相應的引用去指向運行時常量池。

常量池對應的類ConstantPool,它也實現了Metadata這個元數據類。根據上面的信息,我們可以猜測,實現了Metadata的相關子類,都會是方法區中的成員,主要包括Klass、ConstantPool、Method等。還有一個點是很值得關注的,源碼的注釋中有這樣的一句話:Constant pool for this class,也就是說在方法區中有很多個常量池,每個類會對應自己的一個專屬常量池,在instanceKlass對象中很多字段存放的都是在常量池中的索引

1.7 針對Oop/Class/Klass做個小總結

最終形成的就是如下這樣的一個結構,算是對整個java對象的模型的形象描述。

image.png

補充:

  • 1.實際上java/lang/Class對象也是使用oopDesc去進行表示的,也就是它和我們普通的java對象本質上來說是沒什么區別的。
  • 2.實際上Klass的鏡像java/lang/Class對象中也有保存Klass的指針(雙向引用),這里暫時忽略。

1.8 HotSpotVM中方法區的動態性

HotSpotVM中方法區中的內容并不是恒定不變的,它完全是動態的,也就是說在運行時還完全使用類加載器去加載相關的字節碼信息進入方法區。有什么作用?

最典型的應用就是去完成運行時動態代理,相信使用過Spring Framework這個框架的小伙伴們應該是比較熟悉SpringAOP的動態代理的(包括JDK的動態搭理、CGLIB的動態代理甚至是AspectJ的動態代理)。

1.8.1 了解JDK類庫中的defineClass方法

在JDK類庫的ClassLoader類中有下面這樣的native方法defineClass,主要作用就是交給VM,讓它往方法區中加載一個instanceKlass對象并創建相應的鏡像mirror對象。

    static native Class<?> defineClass1(ClassLoader loader, String name, byte[] b, int off, int len,
                                        ProtectionDomain pd, String source);

    static native Class<?> defineClass2(ClassLoader loader, String name, java.nio.ByteBuffer b,
                                        int off, int len, ProtectionDomain pd,
                                        String source);

在大名鼎鼎的Unsafe類中也提供的相關defineClass的方法去供我們使用

    public native Class<?> defineClass0(String name, byte[] b, int off, int len,
                                        ClassLoader loader,
                                        ProtectionDomain protectionDomain);

1.8.2 了解ASM框架動態生成字節碼的基本原理

我們下面使用一個最典型的方式去直接動態加載字節碼。我們使用到一個框架ASM,熟悉C/Cpp的朋友應該從名字來看我們就知道是和匯編相關的,沒錯,這個框架就是和匯編相關的,這是一個字節碼(java的匯編)生成的框架,感興趣的小伙伴可以自行學習ASM框架相關的更多知識。

我這里需要完成的任務是,我想要生成一個com.wanna.asm.HelloWorld類,然后去實現它的toString方法。

既然使用到asm,就得導入相關的依賴(使用maven進行導入)

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <asm.version>9.0</asm.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-commons</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-util</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-tree</artifactId>
      <version>${asm.version}</version>
    </dependency>
    <dependency>
      <groupId>org.ow2.asm</groupId>
      <artifactId>asm-analysis</artifactId>
      <version>${asm.version}</version>
    </dependency>
  </dependencies>

我們編寫如下的java代碼,為了去生成我們的目標類:

public class HelloWorldDump implements Opcodes {

    public static byte[] dump() {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        /**
         * 第一個參數設置字節碼的版本
         * 第二個參數設置類的訪問標識符,這里主要用到ACC_PUBLIC用來設置該類是公有的(public關鍵字),以及ACC_SUPER設置它是有父類的(extends關鍵字)
         * 第三個參數設置我們要訪問的類的類全限定名為com/wanna/asm/HelloWorld
         * 第四個參數設置簽名
         * 第五個參數設置父類的全限定名java/lang/Object
         * 第六個參數設置實現的接口為null
         */
        cw.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/wanna/asm/HelloWorld", null, "java/lang/Object", null);

        //創建目標類的構造器方法
        {
            //創建構造器方法
            MethodVisitor mv1 = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
            //想要訪問一個方法的code部分...
            mv1.visitCode();
            mv1.visitVarInsn(ALOAD, 0);
            //調用Object父類的構造器方法...需要使用到的是invoke special字節碼
            mv1.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            //寫一個return字節碼表示返回空
            mv1.visitInsn(RETURN);
            mv1.visitMaxs(1, 1);
            mv1.visitEnd();
        }
        //創建目標類的toString方法
        {
            //訪問HelloWorld的toString方法,并設置方法描述為()Ljava/lang/String;,也就是返回類型為String,并且參數為空
            MethodVisitor mv2 = cw.visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);
            //想要訪問方法的code部分...
            mv2.visitCode();
            //使用ldc字節碼往棧中壓入一個實例...
            mv2.visitLdcInsn("This is a HelloWorld object.");
            //寫一個areturn字節碼表示返回一個引用類型的對象...
            mv2.visitInsn(ARETURN);
            mv2.visitMaxs(1, 1);
            mv2.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}

主要就提供了一個HelloWorldDump.dump方法,返回的是一個byte數組。但是僅僅這樣做,JVM并不會加載我們的com.wanna.asm.HelloWorld類,我們還需要提供自己的類加載器。(為了簡便,讓它只能加載這個類,加載到其它類直接拋出異常)

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if ("com.wanna.asm.HelloWorld".equals(name)) {
            byte[] bytes = HelloWorldDump.dump();
            return defineClass(name, bytes, 0, bytes.length);
        }
        throw new ClassNotFoundException("Class Not Found: " + name);
    }
}

編寫我們的測試代碼,使用反射去拿到這個類的Class對象:

public class HelloWorldRun {
    public static void main(String[] args) throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> clazz = classLoader.loadClass("com.wanna.asm.HelloWorld");
        Object instance = clazz.getDeclaredConstructor().newInstance();
        System.out.println(instance);
    }
}

得到下面的運行結果

This is a HelloWorld object.

也就是說,我們成功地將我們使用代碼生成的com.wanna.asm.HelloWorld加載到VM中了,在方法區中已經有該類的元信息、在堆中已經有該類的Class對象。

出于好奇,我們就想看看HelloWorldDump生成的byte數組里面究竟是什么東東?我們直接使用輸出流輸出到文件中!

        final String filePath = "/Users/wanna/Desktop/Code/java/jdksrc/jdk_debug/src/main/java/com/wanna/asm";
        try (FileOutputStream fos = new FileOutputStream(filePath + "/" + "HelloWorld.class")) {
            fos.write(HelloWorldDump.dump());
        }

使用javap -v com.wanna.asm.HelloWorld命令看到如下內容:

image.png

其實我們生成的byte數組,就是字節碼呀!而我們ASM框架的作用就是幫助我們生成這樣一個的符合.class文件規范的byte數組,然后再交給類加載器去進行加載到VM當中罷了。

了解到這里之后,相信你已經開始了解VM的動態性了,我們再深一步,去看看JDK動態代理的源碼怎么樣?

1.8.3 了解JDK類庫提供的Proxy.newInstance的方法源碼

我們編寫下面的測試代碼

public class JDKProxy {
    public interface ProxyTest {
        public String getName();
    }

    static class ProxyTestImpl implements ProxyTest {
        @Override
        public String getName() {
            return "wanna";
        }
    }

    static class InvocationImpl<T> implements InvocationHandler {
        T target;

        public InvocationImpl(T target) {  //必須得把target傳進來,不然沒有目標對象的實現類調用不了...
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object ret = method.invoke(target, args);
            ret += "666";
            return ret;
        }
    }

    public static void main(String[] args) throws Exception {
        Object instance = Proxy.newProxyInstance(JDKProxy.class.getClassLoader(),
                new Class<?>[]{ProxyTest.class},
                new InvocationImpl<ProxyTest>(new ProxyTestImpl()));

        Method method = instance.getClass().getMethod("getName");
        Object ret = method.invoke(instance);
        System.out.println(ret);
    }
}

我們點進去Proxy.newInstance方法的源碼。

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {
        final Class<?> caller = System.getSecurityManager() == null
                                    ? null
                                    : Reflection.getCallerClass();
        /*
         * Look up or generate the designated proxy class and its constructor.
         */  //查找或生成指定的代理類及其構造函數。
        Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);  //獲取到構造器

        return newProxyInstance(caller, cons, h);  //使用Constructor#newInstance去new一個對象
    }

我們主要關心getProxyConstructor方法:

    private static Constructor<?> getProxyConstructor(Class<?> caller,
                                                      ClassLoader loader,
                                                      Class<?>... interfaces)
    {
        // optimization for single interface
        if (interfaces.length == 1) {  //如果接口的數量是一個
            Class<?> intf = interfaces[0];  //獲取到接口就行
            if (caller != null) {
                checkProxyAccess(caller, loader, intf);
            }
            return proxyCache.sub(intf).computeIfAbsent( //在computeIfAbsent里面計算出來代理類的構造器
                loader,
                (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()    //最終會回調ProxyBuilder#build,在里面創建字節碼文件
            );
        } else {  //如果接口有很多個,需要克隆一份接口出來去完成...
            // interfaces cloned
            final Class<?>[] intfsArray = interfaces.clone();  //把接口克隆一份
            if (caller != null) {
                checkProxyAccess(caller, loader, intfsArray);
            }
            final List<Class<?>> intfs = Arrays.asList(intfsArray);
            return proxyCache.sub(intfs).computeIfAbsent(
                loader,
                (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()
            );
        }
    }

我們主要關心ProxyBuilder.build方法:

        Constructor<?> build() {
            Class<?> proxyClass = defineProxyClass(module, interfaces);  //獲取到Class對象(通過寫字節碼,再使用類加載器加載進來)
            final Constructor<?> cons;
            try {
                cons = proxyClass.getConstructor(constructorParams);  //從代理類Class對象獲取構造器
            } catch (NoSuchMethodException e) {
                throw new InternalError(e.toString(), e);
            }
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);  //設置構造器為可訪問的
                    return null;
                }
            });
            return cons;
        }

我們似乎看到了熟悉的代碼defineProxyClassgetConstructor,我們主要關心defineProxyClass方法

        //定義即將要代理的類Class對象(通過直接寫字節碼進行生成)...
        private static Class<?> defineProxyClass(Module m, List<Class<?>> interfaces) {
            String proxyPkg = null;     // package to define proxy class in
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;  //設置訪問標志位為PUBLIC|FINAL
       //------------------------------此處省略n行代碼-----------------------------------------------
            /*
             * Choose a name for the proxy class to generate.
             */
            //在代理的包后面跟上$Proxy前綴以及一個唯一的數(nextUniqueNumber)
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg.isEmpty()
                                    ? proxyClassNamePrefix + num
                                    : proxyPkg + "." + proxyClassNamePrefix + num;

            ClassLoader loader = getLoader(m);  //獲取到類加載器
            trace(proxyName, m, loader, interfaces);

            /*
             * Generate the specified proxy class.
             */  //生成特殊的代理類的二進制的字節碼數組,里面會寫字節碼到一個byte數組當中,寫字段、方法、字節碼等...
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                    proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
            try {  //通過UNSAFE類去定義一個類,它是個native方法,是交給JVM去做了...
                Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile,
                                                 0, proxyClassFile.length,
                                                 loader, null);
                reverseProxyCache.sub(pc).putIfAbsent(loader, Boolean.TRUE);
                return pc;
            } catch (ClassFormatError e) {
                /*
                 * A ClassFormatError here means that (barring bugs in the
                 * proxy class generation code) there was some other
                 * invalid aspect of the arguments supplied to the proxy
                 * class creation (such as virtual machine limitations
                 * exceeded).
                 */
                throw new IllegalArgumentException(e.toString());
            }
        }

我們可以很明顯的看到這里生成了一個byte數組,然后使用UNSAFE.defineClass去將這個類加載到VM中,終于破案了!也就是說JDK動態代理最終的原理也就是創建一個byte數組(字節碼),然后交給VM去進行加載,和我們之前的使用ASM框架去生成代碼的原理基本上類似。

1.8.4 針對方法區動態性的小總結

JDK動態代理使用的就是Proxy.newInstance方法去通過生成byte數組從而去defineClass,然后去new的對象。而CGLIB動態代理呢?則是采用第一種方式,也就是ASM框架的方式去完成的動態代理,也是想方設法生成字節碼數組,最終交給VM去完成defineClass,再完成對象的創建。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容