- 深入解析Java虛擬機HotSpot
- 楊易
- 2098字
- 2021-01-07 11:18:28
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所示。
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所示。
使用VM參數-XX:+UseCompressedOops還可以開啟對象指針壓縮,在64位機器上開啟該參數后,可以用32位無符號整數值(narrowOop)來表示oop指針。壓縮對象指針允許32位整數表示64位指針。對象引用位數的減少允許堆中存放更多的其他數據,繼而提高內存利用率,但是隨之而來的問題是64位指針的可尋址范圍可能是0~242字節或0~248字節(一般64位CPU的地址總線到不了64位),壓縮后只能尋址0~232字節,顯然無法覆蓋可能的內存范圍。對于這個問題,HotSpot VM的應對方案如圖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億次。
- iOS面試一戰到底
- Learning Java Functional Programming
- SpringMVC+MyBatis快速開發與項目實戰
- LabVIEW入門與實戰開發100例
- 深度強化學習算法與實踐:基于PyTorch的實現
- C++ 從入門到項目實踐(超值版)
- Nginx Lua開發實戰
- Learning Apache Karaf
- Visual C#.NET Web應用程序設計
- Microsoft 365 Certified Fundamentals MS-900 Exam Guide
- Lift Application Development Cookbook
- 嵌入式Linux C語言程序設計基礎教程
- JavaScript Concurrency
- Android應用開發攻略
- Learn Linux Quickly