1. 程式人生 > 其它 >深入理解JVM - HotSpot虛擬機器物件

深入理解JVM - HotSpot虛擬機器物件

物件建立

僅限於普通Java物件,不包括陣列和Class物件;不包括複製(克隆?)、反序列化。

  1. 類載入檢查:遇到位元組碼new指令,檢查指令引數能否在常量池定位到一個類的符號引用,並檢查這個符號引用對應的類是否已被載入、解析和初始化過,如果沒有則執行對應的類載入過程
  2. 分配記憶體:物件所需記憶體大小在類載入完成後便可完全確定。
    1. 記憶體分配方式
      1. 指標碰撞:Bump The Pointer,要求Java堆記憶體絕對規整,使用過的記憶體放一邊,未使用的放一邊,中間指標隔開,記憶體分配僅是移動指標
      2. 空閒連結串列:Free List,Java堆記憶體不是絕對規整,則必須維護列表,記錄那些記憶體可用,分配時從連結串列找到一塊足夠大的記憶體分配給物件例項
      3. 選擇哪種分配方式由Java堆是否規整決定,Java堆是否規整又由所採用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定
      4. Serual、ParNew:帶壓縮整理過程的收集器,使用指標碰撞,簡單高效
      5. CMS:基於清除(Sweep)演算法,可先通過空閒列表得到大塊分配緩衝區(Linear Allocation Buffer),然後在這個快取區中使用指標碰撞的方式分配記憶體
    2. 執行緒安全
      1. 記憶體分配同步處理:CAS + 失敗重試保證更新原子性
      2. 每個執行緒預先分配大塊的TLAB(本地執行緒分配緩衝,Thread Local Allocation Buffer),只有這一步需要同步,為物件分配記憶體時線上程私有的TLAB中進行分配
  3. 物件記憶體初始化:將分配給物件的記憶體初始化為0值(但不含物件頭,為什麼?),如果使用了TLAB,這一步可提前至TLAB分配時執行(這裡顯然對物件頭一同初始化了),保證物件例項欄位不用賦初值即可使用。
  4. 物件頭初始化
  5. 物件初始化:即建構函式的執行,由new指令後是否跟隨invokespecial指令決定(一般編譯器會在new關鍵字處同時生成兩個位元組碼指令,但其他方式產生的則不一定)

HotSpot虛擬機器位元組碼直譯器中的程式碼片段

// 確保常量池中存放的是已解釋的類 
if (!constants->tag_at(index).is_unresolved_klass()) { 
    
// 斷言確保是klassOop和instanceKlassOop(這部分下一節介紹) 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(); // 確保物件所屬型別已經經過初始化階段 if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) { // 取物件長度 size_t obj_size = ik->size_helper(); oop result = NULL; // 記錄是否需要將物件所有欄位置零值 bool need_zero = !ZeroTLAB; // 是否在TLAB中分配物件 if (UseTLAB) { result = (oop) THREAD->tlab().allocate(obj_size); } if (result == NULL) { need_zero = true; // 直接在eden中分配物件 retry: HeapWord* compare_to = *Universe::heap()->top_addr(); HeapWord* new_top = compare_to + obj_size; // cmpxchg是x86中的CAS指令,這裡是一個C++方法,通過CAS方式分配空間,併發失敗的 話,轉到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) { // 如果需要,為物件初始化零值 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); } } // 根據是否啟用偏向鎖,設定物件頭資訊 if (UseBiasedLocking) { result->set_mark(ik->prototype_header()); } else { result->set_mark(markOopDesc::prototype()); } result->set_klass_gap(0); result->set_klass(k_entry); \ // 將物件引用入棧,繼續執行下一條指令 SET_STACK_OBJECT(result, 0); UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1); } } }

物件記憶體佈局

HotSpot中,物件在堆記憶體中的佈局可分為三個部分:物件頭、例項資料、對齊填充

物件頭

  1. Mark Word:物件自身執行時資料,雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。在32位和64位的虛擬機器(未開啟指標壓縮)中分別32和64位元,官方稱之為"Mark Word"。是一個動態資料結構,根據物件狀態複用儲存空間
  2. 型別指標:只想物件的型別元資料的指標,JVM通過它來確定物件是哪個類的例項,並非所有虛擬機器都保留型別指標
  3. 陣列長度:陣列物件有

例項資料

  1. 無論是從父類繼承下來的,還是子類中定義的欄位,都必須記錄下來
  2. 儲存順序受虛擬機器分配策略引數(-XX:FieldsAllocationStyle)和欄位原始碼定義順序影響
  3. HotSpot預設分配順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),相同寬度存放在一起,在此前提下,父類定義變數在子類之前
  4. 若HotSpot虛擬機器引數+XX:CompactFields引數值為true(預設就為true),那子類中較窄的變數也允許插入父類變數空隙之中,以節省空間

對齊填充

  1. 不是非必要的,僅起到佔位符作用
  2. 這部分的存在是因為HotSpot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,也就是說加上佔位符,任何物件的大小都必須是8位元組的整數倍
  3. 物件頭剛好是8位元組整數倍(1或2倍)

物件訪問定位

Java程式通過棧上的reference操作堆上的具體物件。《JVM規範》僅規定了reference是一個指向物件的引用,並未規定這個引用該通過什麼方式去定位、訪問到堆中物件的具體位置。

主流訪問方式主要有使用控制代碼和直接指標兩種。

控制代碼

Java堆中將可能會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自具體的地址資訊

直接指標

Java堆中物件的記憶體佈局就必須考慮如何放置訪問型別資料的相關資訊,reference中儲存的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷

直接指標:速度更快,它節省了一次指標定位的時間開銷,HotSpot使用它

問題

  1. private的父類變數是否會被繼承下來?父子類相同變數,父類方法訪問,子類變數覆蓋,那麼子類呼叫訪問的是哪個?子類重寫方法,訪問的是哪個?
  2. 一個物件最大為多大