官术网_书友最值得收藏!

3.2 對象

虛擬機中的對象由oop表示。oop的全稱是Ordinary Object Pointer,它來源于Smalltalk和Self語言,字面意思是“普通對象指針”,在HotSpot VM中表示受托管的對象指針。“受托管”是指該指針能被虛擬機的各組件跟蹤,如GC組件可以在發現對象不再使用時回收其內存,或者可以在發現對象年齡過大時,將對象移動到另一個內存分區等。總地來說,對象是由對象頭和字段數據組成的。

3.2.1 創建對象

創建oop的藍圖是InstanceKlass。InstanceKlass了解對象所有信息,包括字段個數、大小、是否為數組、是否有父類,它能根據這些信息調用InstanceKlass::allocate_instance創建對應的instanceOop/arrayOop,如代碼清單3-1所示:

代碼清單3-1 allocate_instance


instanceOop InstanceKlass::allocate_instance(TRAPS) {
    bool has_finalizer_flag = has_finalizer(); // 是否重寫finalizer方法
    int size = size_helper();                  // 獲取對象大小
    instanceOop i;
    // 在堆上分配對象
    i = (instanceOop)Universe::heap()->obj_allocate(...);
    if (has_finalizer_flag && !RegisterFinalizersAtInit) {
        i = register_finalizer(i, CHECK_NULL);
    }
    return i;                                  // 返回對象
}
oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
    ObjAllocator allocator(klass, size, THREAD);
    return allocator.allocate();
}

這里虛擬機調用Java堆的CollectedHeap::obj_allocate創建對象。Obj_allocate內部又使用ObjAllocator創建對象。ObjAllocator做的事情很簡單,如代碼清單3-2所示:

代碼清單3-2 內存分配與對象創建


oop MemAllocator::allocate()const{     // ObjAllocator繼承自MemAllocator
    oop obj = NULL;
    Allocation allocation(*this, &obj);
    // 根據對象size分配一片內存
    HeapWord* mem = mem_allocate(allocation);
    if(mem != NULL) {
        // 初始化對象頭,initialize會返回oop(mem)
        obj = initialize(mem);   
    }
    return obj;
}

代碼清單3-2揭示了Java對象的實質:一片內存。虛擬機首先獲知對象大小,然后申請一片內存(mem_allocate),返回這片內存的首地址(HeapWord,完全等價于char*指針)。接著初始化(initialize)這片內存最前面的一個機器字,將它設置為對象頭的數據。然后將這片內存地址強制類型轉換為oop(oop類型是指針)返回,最后由allocate_instance再將opp強制類型轉換為instanceOop返回。

有很多方法可以查看oop對象布局,了解它有助于深刻理解HotSpot VM的對象實現。使用-XX:+PrintFieldLayout虛擬機參數可以輸出對象字段的偏移,但是該參數的輸出內容比較簡略。要想獲取詳細的對象布局,可以使用JOL(Java Object Layout)工具,但JOL不是JDK自帶的工具,需要自行下載。除了JOL外,還可以使用JDK自帶的jhsdb工具獲取。使用jhsdb hsdb命令打開HotSpot Debugger程序,可以查看oop的內部數據,如圖3-2所示。

圖3-2 使用jhsdb hsdb命令查看oop的內部數據

oop最開始的兩個字段是_mark和_metadata,它們包含一些對象的元數據,接著是包含對象字段的數據。下面將詳細介紹_mark和_metadata的內容。

3.2.2 對象頭

了解“oop是指向一片內存的指針,只是將這片內存‘視作’(強制類型轉換)Java對象/數組”十分重要,因為對象的本質就是用對象頭和字段數據填充這片內存。對象頭即oopDesc,它只有兩個字段,如代碼清單3-3所示:

代碼清單3-3 對象頭結構


class oopDesc {
    volatile markOop _mark;
    union _metadata {
        Klass*      _klass;
        narrowKlass _compressed_klass;
    } _metadata;
};

對象頭的第一個字段是_mark,也叫Mark Word,雖然由于歷史原因帶了個oop字樣,但是它與oop并沒有關系。它在形式上是一個指針,但是HotSpot VM把它當作一個整數來使用。根據CPU位數,markOop表現為32位或者64位整數,不同位(bit)有不同意義,如圖3-3所示。

圖3-3 markOop

使用VM參數-XX:+UseCompressedOops還可以開啟對象指針壓縮,在64位機器上開啟該參數后,可以用32位無符號整數值(narrowOop)來表示oop指針。壓縮對象指針允許32位整數表示64位指針。對象引用位數的減少允許堆中存放更多的其他數據,繼而提高內存利用率,但是隨之而來的問題是64位指針的可尋址范圍可能是0~242字節或0~248字節(一般64位CPU的地址總線到不了64位),壓縮后只能尋址0~232字節,顯然無法覆蓋可能的內存范圍。對于這個問題,HotSpot VM的應對方案如圖3-4所示,其中壓縮對象指針有三種尋址模式:

圖3-4 壓縮對象指針尋址

? 如果堆的高位地址小于32GB,說明不需要基址(base)就能定位堆中任意對象,這種模式也叫作零地址Oop壓縮模式(Zero-based Compressed Oops Mode);

? 如果堆的高位大于等于32GB,說明需要基址,這時如果堆大小小于4GB,說明基址+偏移能定位堆中任意對象;

? 如果堆大小處于4~32GB,這時只能通過基址+偏移×縮放(scale)才能定位堆中任意對象。

這三種尋址模式最大支持32GB的堆,很顯然,如果Java堆大于32GB,那么將無法使用壓縮對象指針。

對象頭的第二個字段_metadata表示對象關聯的類(klass)。它是union類型,_klass表示正常的指針,另一個narrowKlass是針對64位CPU的優化。如果開啟-XX:+UseCompressedClassPointers,虛擬機會將指向klass的指針壓縮為一個無符號32位整數(_compressed_klass),剩下的32位則用于存放對象字段數據,如果是typeArrayOop或objArrayOop,還能存放數組長度。但是壓縮klass指針也會遇到和壓縮對象指針一樣的問題,即尋址范圍無法覆蓋可能的內存區域,對此,HotSpot VM的解決方案也是使用基址+偏移×縮放進行定位,只是這時候32位無符號整數偏移是narrowKlass而不是narrowOop。

3.2.3 對象哈希值

_mark中有一個hash code字段,表示對象的哈希值。每個Java對象都有自己的哈希值,如果沒有重寫Object.hashCode()方法,那么虛擬機會為它自動生成一個哈希值。哈希值生成的策略如代碼清單3-4所示:

代碼清單3-4 對象hash值生成策略


static inline intptr_t get_next_hash(Thread * Self, oop obj) {
    intptr_t value = 0;
    if (hashCode == 0) {         // Park-Miller隨機數生成器
        value = os::random();
    } else if (hashCode == 1) {  // 每次STW時生成stwRandom做隨機
        intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3;
        value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom;
    } else if (hashCode == 2) {  // 所有對象均為1,測試用的
        value = 1;
    } else if (hashCode == 3) {  // 每創建一個對象,hash值加一
        value = ++GVars.hcSequence;  
    } else if (hashCode == 4) {  // 將對象內存地址當作hash值
        value = cast_from_oop<intptr_t>(obj); 
    } else { // Marsaglia xor-shift 隨機數算法,生成hashcode
        unsigned t = Self->_hashStateX;
        t^= (t << 11);
        Self->_hashStateX = Self->_hashStateY;
        Self->_hashStateY = Self->_hashStateZ;
        Self->_hashStateZ = Self->_hashStateW;
        unsigned v = Self->_hashStateW;
        v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
        Self->_hashStateW = v;
        value = v;
    }
    value &= markOopDesc::hash_mask;
    if (value == 0) value = 0xBAD;
    return value;
}

Java層調用Object.hashCode()或者System.identityHashCode(),最終會調用虛擬機層的runtime/synchronizer的get_next_hash()生成哈希值。get_next_hash內置六種可選方案,如代碼清單3-4所示,可以使用-XX:hashCode=<val>指定生成策略。OpenJDK 12目前默認的策略是Marsaglia XOR-Shift隨機數生成器,它通過重復異或和位移自身值,可以得到一個很長的隨機數序列周期,生成的隨機數序列通過了所有隨機性測試。另外,它的速度也非常快,能達到每秒2億次。

主站蜘蛛池模板: 甘肃省| 应城市| 西乌珠穆沁旗| 洪洞县| 章丘市| 甘谷县| 蒙山县| 宁明县| 广宁县| 南充市| 新邵县| 中牟县| 丰县| 安溪县| 文登市| 钦州市| 鄄城县| 洪洞县| 邢台县| 凯里市| 社旗县| 潞西市| 磐石市| 囊谦县| 正宁县| 井陉县| 封开县| 肇州县| 汪清县| 富阳市| 侯马市| 比如县| 望都县| 济南市| 贺州市| 富宁县| 云霄县| 广元市| 宝山区| 资阳市| 大冶市|