OopMapBlock是一個簡單的內嵌在Klass里面的數據結構,用來描述oop中包含的引用類型屬性,即該oop所引用的其他oop在oop中的內存分布,然后就可以根據當前oop的地址找到所有引用的其他oop了,其定義如下:
源代碼位置:oops/instanceKlass.hpp // ValueObjs embedded in klass. Describes where oops are located in instances of // this klass. class OopMapBlock VALUE_OBJ_CLASS_SPEC { public: // Byte offset of the first oop mapped by this block. int offset() const { return _offset; } void set_offset(int offset) { _offset = offset; } // Number of oops in this block. uint count() const { return _count; } void set_count(uint count) { _count = count; } // sizeof(OopMapBlock) in HeapWords. static const int size_in_words() { return align_size_up(int(sizeof(OopMapBlock)), HeapWordSize) >> LogHeapWordSize; } private: int _offset; uint _count; };
OopMapBlock結構可以描述某個對象中引用區域的起始偏移和引用個數。offset描述第一個所引用的oop相對於當前oop地址的偏移量,count表示包含的oop的個數,注意這里的包含並不是指這些oop位於OopMapBlock里面,而是有count個連續存放的oop。為啥會有多個OopMapBlock了?因為每個OopMapBlock只能描述當前子類中包含的引用類型屬性,父類的引用類型屬性由單獨的OopMapBlock描述。
之前介紹過,可以利用-XX:+PrintFieldLayout來查看布局情況。該選項只在調試版本中有效。至於布局模式,可以使用-XX:+FieldsAllocationStyle=mode來指定,默認是1。之前介紹過布局模式有3種,如下:
- allocation_style=0,字段排列順序為oops、longs/doubles、ints、shorts/chars、bytes,最后是填充字段,以滿足對齊要求;
- allocation_style=1,字段排列順序為longs/doubles、ints、shorts/chars、bytes、oops,最后是填充字段,以滿足對齊要求;
- allocation_style=2,JVM在布局時會盡量使父類oops和子類oops挨在一起。
當allocation_style的值為2時,父子oop的布局會連續在一起,這樣至少有2個好處:
- 減少OopMapBlock的數量。由於GC收集時要掃描存活的對象,所以必須知道對象中引用的內存位置。原始類型不需要掃描。
- 連續的對象區域使得緩存行的使用效率更高。試想如果父對象和子對象的對象引用區域不連續,而中間插入了原始類型字段的話,那么在做GC對象掃描時,很可能需要跨緩存行讀取才能完成掃描。
下面我們用HSDB來實際探查下相關的內存布局:
package jvmTest; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; class Base{ private int a=1; private String s="abc"; private Integer a2=12; private int a3=22; } class A extends Base { private int b=3; private String s2="def"; private int b2=33; private Base a=new Base(); } class B extends A{ private String s3="ghk"; private Integer c=4; private int c2=44; } public class jvmTest { public static void main(String[] args) { Base a=new Base(); A a2=new A(); B b=new B(); while (true){ try { System.out.println(getProcessID()); Thread.sleep(600*1000); } catch (Exception e) { } } } public static final int getProcessID() { RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); System.out.println(runtimeMXBean.getName()); return Integer.valueOf(runtimeMXBean.getName().split("@")[0]).intValue(); } }
運行main方法后,用HSDB查看main線程的線程棧,從中找出變量a,a2,b對應的oop的地址,如下:
分別查看0x00000000d69d98a0,0x00000000d69db5f8,0x00000000d69dd070對應的oop,如下:
上從面的截圖可知,子類會完整的保留父類的屬性,從而方便調用父類方法時能夠正確的使用父類的屬性。上述對oop的屬性打印是按照類聲明屬性的順序來的,內存中是這樣保存的么?可以通過查看屬性的偏移量來判斷。
在Class Browser中搜索jvmTest,可以查找到我們三個自定義類對應的Klass,如下圖:
分別點擊這三個類查看屬性的偏移量,如下:
根據上述偏移量,我們可以得出jvmTest.B對象的內存布局,如下:
int本身占4個字節,引用類型屬性本質上就是一個指針,這里因為默認開啟了指針壓縮,所以也是4字節。
我們再看下表示OopMapBlock在Klass中的字寬數的屬性_nonstatic_oop_map_size在三個類中的取值,如下:
OopMapBlock本身就只有兩個int屬性,所以一個OopMapBlock實例只有8字節,即一個字寬,jvmTest.B的_nonstatic_oop_map_size屬性值為3,即由3個OopMapBlock,下面通過CHSDB的mem命令來看看這3個OopMapBlock對應的內存數據。
首先執行inspect對象得到該Klass本身的大小,即sizeof的大小,如下:
vtable,itable,OopMapBlock這三個都是內嵌在Klass里面的,所謂的內嵌實際是指這塊內存是緊挨着Klass自身的屬性對應的內存的下面,從上一節的分析可知,OopMapBlock在itable的后面,itable在vtable的后面,而vtable是緊挨着Klass的,從上述inspect命令的輸出,也可知道itable和vtable的內存大小,單位是字寬,如下:
因此OopMapBlock的起始地址就是Klass的地址加上Klass本身的大小440字節即55字寬,再加上vtable的5字寬,itable的2字寬,總共加62字寬,OopMapBlock本身占用3個字寬,因此用mem查看這65字寬的數據,如下:
最后的3個字寬如下:
每個字寬對應一個OopMapBlock,前面4字節就是count屬性,這里都是2,后面4字節就是offset,分別是20,36,48,與jvmTest.B的內存結構是完全一致的。
在parseClassFile()方法中開辟了OopMapBlock的內存空間后,還會調用方法來填充OopMapBlock,這都是在解析Class文件的階段完成的,如下:
// Compute transitive closure(閉包) of interfaces this class implements // Do final class setup fill_oop_maps(this_klass, info.nonstatic_oop_map_count, info.nonstatic_oop_offsets, info.nonstatic_oop_counts);
調用的fill_oop_maps()方法的實現如下:
void ClassFileParser::fill_oop_maps(instanceKlassHandle k, unsigned int nonstatic_oop_map_count, int* nonstatic_oop_offsets, unsigned int* nonstatic_oop_counts) { OopMapBlock* this_oop_map = k->start_of_nonstatic_oop_maps(); const InstanceKlass* const super = k->superklass(); const unsigned int super_count = super ? super->nonstatic_oop_map_count() : 0; if (super_count > 0) { // Copy maps from superklass OopMapBlock* super_oop_map = super->start_of_nonstatic_oop_maps(); for (unsigned int i = 0; i < super_count; ++i) { *this_oop_map++ = *super_oop_map++; } } if (nonstatic_oop_map_count > 0) { if (super_count + nonstatic_oop_map_count > k->nonstatic_oop_map_count()) { // The counts differ because there is no gap between superklass's last oop // field and the first local oop field. Extend the last oop map copied // from the superklass instead of creating new one. nonstatic_oop_map_count--; nonstatic_oop_offsets++; this_oop_map--; this_oop_map->set_count(this_oop_map->count() + *nonstatic_oop_counts++); this_oop_map++; } // Add new map blocks, fill them while (nonstatic_oop_map_count-- > 0) { this_oop_map->set_offset(*nonstatic_oop_offsets++); this_oop_map->set_count(*nonstatic_oop_counts++); this_oop_map++; } assert(k->start_of_nonstatic_oop_maps() + k->nonstatic_oop_map_count() == this_oop_map, "sanity"); } }
相關文章的鏈接如下:
1、在Ubuntu 16.04上編譯OpenJDK8的源代碼
13、類加載器
14、類的雙親委派機制
15、核心類的預裝載
16、Java主類的裝載
17、觸發類的裝載
18、類文件介紹
19、文件流
20、解析Class文件
21、常量池解析(1)
22、常量池解析(2)
23、字段解析(1)
24、字段解析之偽共享(2)
25、字段解析(3)
作者持續維護的個人博客classloading.com。
關注公眾號,有HotSpot源碼剖析系列文章!
參考文章:
(1)https://zhuanlan.zhihu.com/p/28226360
(2)https://blog.csdn.net/lqp276/article/details/52190503
(3)https://blog.csdn.net/qq_31865983/article/details/104284546#t3
(6)https://blog.csdn.net/qq_31865983/article/details/104284546#4%E3%80%81OopMapBlock