溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Java技術(shù)JVM研究中HotSpot虛擬機(jī)對(duì)象的示例分析

發(fā)布時(shí)間:2021-09-18 10:42:05 來(lái)源:億速云 閱讀:123 作者:柒染 欄目:編程語(yǔ)言

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)?lái)有關(guān)Java技術(shù)JVM研究中HotSpot虛擬機(jī)對(duì)象的示例分析,文章內(nèi)容豐富且以專(zhuān)業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

對(duì)象的創(chuàng)建


語(yǔ)言層面上,創(chuàng)建對(duì)象通常(例外:克隆、反序列化)僅僅是一個(gè) new 關(guān)鍵字而已,而在虛擬機(jī)中,對(duì)象(本文中討論的對(duì)象限于普通 Java 對(duì)象,不包括數(shù)組和 Class 對(duì)象等)的創(chuàng)建又是怎樣一個(gè)過(guò)程呢?


虛擬機(jī)遇到一條 new 指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載、解析和初始化過(guò)的。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。


內(nèi)存的分配

類(lèi)加載通過(guò)后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類(lèi)加載完成后便可完全確定(如何確定在下一節(jié)對(duì)象內(nèi)存布局時(shí)再詳細(xì)講解),為對(duì)象分配空間的任務(wù)具體便等同于一塊確定大小的內(nèi)存從 Java 堆中劃分出來(lái),怎么劃呢?


指針碰撞

假設(shè) Java 堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱(chēng)為“指針碰撞”(Bump The Pointer)。


空閑列表

Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒(méi)有辦法簡(jiǎn)單的進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱(chēng)為“空閑列表”(Free List)。

內(nèi)存分配選擇


選擇哪種分配方式由 Java 堆是否規(guī)整決定,而 Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。 因此在使用 SerialParNew 等帶 Compact 過(guò)程的收集器時(shí),系統(tǒng)采用的分配算法是指針碰撞,而使用 CMS 這種基于 Mark-Sweep 算法的收集器時(shí)(說(shuō)明一下,CMS 收集器可以通過(guò) UseCMSCompactAtFullCollectionCMSFullGCsBeforeCompaction 來(lái)整理內(nèi)存),就通常采用空閑列表。


內(nèi)存分配問(wèn)題

如何劃分可用空間之外,還有另外一個(gè)需要考慮的問(wèn)題是對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢茫诓l(fā)情況下也并不是線(xiàn)程安全的,可能出現(xiàn)正在給對(duì)象 A 分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象 B 又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存。**解決這個(gè)問(wèn)題有兩個(gè)方案,一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步——實(shí)際上虛擬機(jī)是采用 CAS 配上失敗重試的方式保證更新操作的原子性;另外一種是把內(nèi)存分配的動(dòng)作按照線(xiàn)程劃分在不同的空間之中進(jìn)行,即每個(gè)線(xiàn)程在 Java 堆中預(yù)先分配一小塊內(nèi)存,稱(chēng)為本地線(xiàn)程分配緩沖,(TLAB ,Thread Local Allocation Buffer),哪個(gè)線(xiàn)程要分配內(nèi)存,就在哪個(gè)線(xiàn)程的 TLAB 上分配, 只有 TLAB 用完,分配新的 TLAB 時(shí)才需要同步鎖定。虛擬機(jī)是否使用 TLAB,可以通過(guò) -XX:+/-UseTLAB 參數(shù)來(lái)設(shè)定。內(nèi)存分配完成之后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),如果使用 TLAB 的話(huà),這一個(gè)工作也可以提前至 TLAB 分配時(shí)進(jìn)行。這步操作保證了對(duì)象的實(shí)例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類(lèi)型所對(duì)應(yīng)的零值。


對(duì)象頭參數(shù)配置

虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的 GC 分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前的運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式

在虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了。 Java 程序的視角看來(lái),對(duì)象創(chuàng)建才剛剛開(kāi)始——方法還沒(méi)有執(zhí)行,所有的字段都為零呢。所以一般來(lái)說(shuō)(由字節(jié)碼中是否跟隨有 invokespecial 指令所決定),new 指令之后會(huì)接著就是執(zhí)行方法,把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)。

下面代碼是 HotSpot 虛擬機(jī) bytecodeInterpreter.cpp 中的代碼片段(這個(gè)解釋器實(shí)現(xiàn)很少機(jī)會(huì)實(shí)際使用,大部分平臺(tái)上都使用模板解釋器;當(dāng)代碼通過(guò) JIT 編譯器執(zhí)行時(shí)差異就更大了。不過(guò)這段代碼用于了解 HotSpot 的運(yùn)作過(guò)程是沒(méi)有什么問(wèn)題的)。

// 確保常量池中存放的是已解釋的類(lèi) 
if (!constants->tag_at(index).is_unresolved_klass()) { 
   // 斷言確保是 klassOop 和 instanceKlassOop(這部分下一節(jié)介紹) 
   oop entry = (klassOop) *constants->obj_at_addr(index); 
   assert(entry->is_klass(), "Should be resolved klass"); 
   klassOop k_entry = (klassOop) entry; 
   assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass"); 
   instanceKlass* ik = (instanceKlass*) k_entry->klass_part(); 
   // 確保對(duì)象所屬類(lèi)型已經(jīng)經(jīng)過(guò)初始化階段 
   if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) { 
       // 取對(duì)象長(zhǎng)度 
       size_t obj_size = ik->size_helper(); 
       oop result = NULL; 
       // 記錄是否需要將對(duì)象所有字段置零值 
       bool need_zero = !ZeroTLAB; 
       // 是否在 TLAB 中分配對(duì)象 
       if (UseTLAB) { 
           result = (oop) THREAD->tlab().allocate(obj_size); 
       } 
       if (result == NULL) { 
           need_zero = true; 
           // 直接在 eden 中分配對(duì)象 
           retry: 
               HeapWord* compare_to = *Universe::heap()->top_addr(); 
               HeapWord* new_top = compare_to + obj_size; 
               // cmpxchg 是 x86 中的 CAS 指令,這里是一個(gè) C++ 方法,通過(guò) CAS 方式分配空間,并發(fā)失敗的話(huà),轉(zhuǎn)到 retry 中重試直至成功分配為止 
               if (new_top <= *Universe::heap()->end_addr()) { 
                   if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) { 
                       goto retry; 
                   } 
                   result = (oop) compare_to; 
               } 
       } 
       if (result != NULL) { 
           // 如果需要,為對(duì)象初始化零值 
           if (need_zero ) { 
               HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize; 
               obj_size -= sizeof(oopDesc) / oopSize; 
               if (obj_size > 0 ) { 
                   memset(to_zero, 0, obj_size * HeapWordSize); 
               } 
           } 
           // 根據(jù)是否啟用偏向鎖,設(shè)置對(duì)象頭信息 
           if (UseBiasedLocking) { 
               result->set_mark(ik->prototype_header()); 
           } else { 
               result->set_mark(markOopDesc::prototype()); 
           } 
           result->set_klass_gap(0); 
           result->set_klass(k_entry); 
           // 將對(duì)象引用入棧,繼續(xù)執(zhí)行下一條指令 
           SET_STACK_OBJECT(result, 0); 
           UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1); 
       } 
   } 
}

對(duì)象的內(nèi)存布局

HotSpot 虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。


HotSpot 虛擬機(jī)的對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC 分代年齡、鎖狀態(tài)標(biāo)志、線(xiàn)程持有的鎖、偏向線(xiàn)程 ID、偏向時(shí)間戳等等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在 32 位和 64 位的虛擬機(jī)(暫不考慮開(kāi)啟壓縮指針的場(chǎng)景)中分別為 32 個(gè)和 64 個(gè) Bits,官方稱(chēng)它為“Mark Word”。


對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出了 32、64 位 Bitmap 結(jié)構(gòu)所能記錄的限度,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間效率,Mark Word 被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。例如在 32 位的 HotSpot 虛擬機(jī)中對(duì)象未被鎖定的狀態(tài)下,Mark Word 的 32 個(gè) Bits 空間中的 25Bits 用于存儲(chǔ)對(duì)象哈希碼(HashCode),4Bits 用于存儲(chǔ)對(duì)象分代年齡,2Bits 用于存儲(chǔ)鎖標(biāo)志位,1Bit 固定為 0,在其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC 標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)內(nèi)容如下表所示。


虛擬機(jī)對(duì)象頭

| 類(lèi)型 | 32位JVM | 64位JVM|
| ------ ---- | ------------| --------- |
| markword | 32bit | 64bit |
| 類(lèi)型指針 | 32bit |64bit ,開(kāi)啟指針壓縮時(shí)為32bit |
| 數(shù)組長(zhǎng)度 | 32bit |32bit |
  1. 開(kāi)啟指針壓縮時(shí),markword占用8bytes,類(lèi)型指針占用8bytes,共占用16bytes;

  2. 未開(kāi)啟指針壓縮時(shí),markword占用8bytes,類(lèi)型指針占用4bytes,但由于java內(nèi)存地址按照8bytes對(duì)齊,長(zhǎng)度必須是8的倍數(shù),因此會(huì)從12bytes補(bǔ)全到16bytes;

  • 數(shù)組長(zhǎng)度為4bytes,同樣會(huì)進(jìn)行對(duì)齊,補(bǔ)足到8bytes;

Java技術(shù)JVM研究中HotSpot虛擬機(jī)對(duì)象的示例分析

  • 如果對(duì)象沒(méi)有重寫(xiě)hashcode方法,那么默認(rèn)是調(diào)用os::random產(chǎn)生hashcode,可以通過(guò)System.identityHashCode獲取;os::random產(chǎn)生

  • hashcode的規(guī)則為:next_rand = (16807seed) mod (2*31-1),因此可以使用31位存儲(chǔ);另外一旦生成了hashcode,JVM會(huì)將其記錄在markword中;

  • GC年齡采用4位bit存儲(chǔ),最大為15,例如MaxTenuringThreshold參數(shù)默認(rèn)值就是15;

  • 當(dāng)處于輕量級(jí)鎖、重量級(jí)鎖時(shí),記錄的對(duì)象指針,根據(jù)JVM的說(shuō)明,此時(shí)認(rèn)為指針仍然是64位,最低兩位假定為0;當(dāng)處于偏向鎖時(shí),記錄的為獲得偏向鎖的線(xiàn)程指針,該指針也是64位;

標(biāo)記字段

32 bits:
  hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
  JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
  size:32 ------------------------------------------>| (CMS free block)
  PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
64 bits:
  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
  size:64 ----------------------------------------------------->| (CMS free block)
 unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
 JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
 narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
 unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

類(lèi)型指針

對(duì)象頭的另外一部分是類(lèi)型指針,即是對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類(lèi)型指針,換句話(huà)說(shuō)查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過(guò)對(duì)象本身,這點(diǎn)我們?cè)谙乱还?jié)討論。另外,如果對(duì)象是一個(gè) Java 數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)因?yàn)樘摂M機(jī)可以通過(guò)普通 Java 對(duì)象的元數(shù)據(jù)信息確定 Java 對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中無(wú)法確定數(shù)組的大小

以下是 HotSpot 虛擬機(jī) markOop.cpp 中的代碼(注釋?zhuān)┢?,它描述?32bits 下 MarkWord 的存儲(chǔ)狀態(tài):

實(shí)例數(shù)據(jù)

接下來(lái)實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也既是我們?cè)诔绦虼a里面所定義的各種類(lèi)型的字段內(nèi)容,無(wú)論是從父類(lèi)繼承下來(lái)的,還是在子類(lèi)中定義的都需要記錄襲來(lái)。

這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在 Java 源碼中定義順序的影響。

HotSpot 虛擬機(jī)默認(rèn)的分配策略為 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿(mǎn)足這個(gè)前提條件的情況下,在父類(lèi)中定義的變量會(huì)出現(xiàn)在子類(lèi)之前。如果 CompactFields 參數(shù)值為 true(默認(rèn)為 true),那子類(lèi)之中較窄的變量也可能會(huì)插入到父類(lèi)變量的空隙之中。

對(duì)齊填充

對(duì)齊填充并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。

由于 HotSpot VM 的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是 8 字節(jié)的整數(shù)倍,換句話(huà)說(shuō)就是對(duì)象的大小必須是 8 字節(jié)的整數(shù)倍。對(duì)象頭部分正好似 8 字節(jié)的倍數(shù)(1 倍或者 2 倍),因此當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊的話(huà),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。

對(duì)象的訪問(wèn)定位

建立對(duì)象是為了使用對(duì)象,我們的 Java 程序需要通過(guò)棧上的 reference 數(shù)據(jù)來(lái)操作堆上的具體對(duì)象。由于 reference 類(lèi)型在 Java 虛擬機(jī)規(guī)范里面只規(guī)定了是一個(gè)指向?qū)ο蟮囊?/strong>,并沒(méi)有定義這個(gè)引用應(yīng)該通過(guò)什么種方式去定位、訪問(wèn)到堆中的對(duì)象的具體位置,對(duì)象訪問(wèn)方式也是取決于虛擬機(jī)實(shí)現(xiàn)而定的。主流的訪問(wèn)方式有使用句柄和直接指針兩種。

如果使用句柄訪問(wèn)的話(huà),Java 堆中將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference 中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)的具體各自的地址信息。如圖 1 所示。

Java技術(shù)JVM研究中HotSpot虛擬機(jī)對(duì)象的示例分析

如果使用直接指針訪問(wèn)的話(huà),Java堆對(duì)象的布局中就必須考慮如何放置訪問(wèn)類(lèi)型數(shù)據(jù)的相關(guān)信息,reference中存儲(chǔ)的直接就是對(duì)象地址,如圖 2 所示。

Java技術(shù)JVM研究中HotSpot虛擬機(jī)對(duì)象的示例分析

這兩種對(duì)象訪問(wèn)方式各有優(yōu)勢(shì),使用句柄來(lái)訪問(wèn)的最大好處就是 reference 中存儲(chǔ)的是穩(wěn)定句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而 reference 本身不需要被修改。

使用直接指針來(lái)訪問(wèn)最大的好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于對(duì)象訪問(wèn)的在 Java 中非常頻繁,因此這類(lèi)開(kāi)銷(xiāo)積小成多也是一項(xiàng)非常可觀的執(zhí)行成本。從上一部分講解的對(duì)象內(nèi)存布局可以看出,就虛擬機(jī) HotSpot 而言,它是使用第二種方式進(jìn)行對(duì)象訪問(wèn),但在整個(gè)軟件開(kāi)發(fā)的范圍來(lái)看,各種語(yǔ)言、框架中使用句柄來(lái)訪問(wèn)的情況也十分常見(jiàn)。

上述就是小編為大家分享的Java技術(shù)JVM研究中HotSpot虛擬機(jī)對(duì)象的示例分析了,如果剛好有類(lèi)似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI