目录
1、类继承结构
2、MetaspaceObj
3、Metadata
4、Klass
5、InstanceKlass
6、Method
7、Java vtable
8、Java itable
9、InstanceKlass特殊子类
10、ArrayKlass
在分析thread.cpp的create_vm函数中(参考《Hotspot启动和初始化源码解析》)发现JVM通过initialize_class函数来加载Java类,该函数是threap.cpp的一个静态函数,其函数定义如下:
接着为main_thread创建thread_object即Java中的java.lang.Thread对象时使用了create_initial_thread函数,该函数返回了oop,实际是类oopDesc* 的别名,如下图:
在《Java程序员自我修养——内存模型》中探讨过,Java对象在内存中是实例数据和类型数据相分离的,实例数据保存了一个指向类型数据的指针,即OOP(ordinary object pointer),因此猜测这里的Klass就是所谓的类型数据,oopDesc就是具体的实例数据了。
在上述代码Klass处按crtl并点击即可进入到定义Klass的头文件kclass.hpp中,该文件的位于hotspot\src\share\vm\oops目录下,选中Klass,点击右键,选择Open Type Hierarchy即可显示该类的继承关系图了,如下:
选择其中某一个类如MetaspaceObj,右键选择Open即可进入定义该父类的头文件alloction.hpp,位于hotspot\src\share\vm\memory下,如下图:
该类是作为存放在Metaspace元空间的中类的基类,不能执行delete,否则会出现致命错误,注意该类没有定义虚函数。该类重载了new操作符,主要用于给共享只读的或者共享读写的类分配内存,该类定义了如下方法:
该类定义了一个枚举Type用于表示对象类型,包含以下几种类型:
Metadata是内部表示类相关元数据的一个基类,注意Metadata定义了多个虚函数,其定义的方法如下:
其中identity_hash()方法返回的实际是该对象的内存地址,如下图:
其中跟stack相关的三个方法是类重定义(class redefinition)期间使用的,跟Java栈帧无关。
一个Klass提供两方面的功能:实现Java语言层面的类和提供多态方法的支持。C++类实例通过保存typeinfo指针实现RTTI,通过vtbl指针实现多态,Hotspot的Oop-Klass模型将这两者整合到Klass中,Java类实例只需保留一个Klass指针即可实现RTTI和多态,能够有效降低指针的内存占用。大致方案是用Oop表示Java实例,主要用于表示实例数据,不提供任何虚函数功能,Oop保存了对应Kclass的指针,所有方法调用通过Klass完成并通过Klass获取类型信息,Klass基于C++的虚函数提供对Java多态的支持。Klass作为父类主要职责是描述了类的继承关系, 其包含的重要属性如下:
_layout_helper:_layout_helper是对象内存布局的一个组合描述符,如果不是InstanceKclass或者ArrayKlass,则该值为0.对InstanceKclass而言,该值表示对象的以字节为单位的内存占用空间,对ArrayKlass而言,该值是一个组合起来的假数字,包含4部分,具体怎么组合和解析由子类实现:
tag:如果数组元素是对象实例则是0x80,否则是0xC0hsz: 数组头元素的字节数ebt:数组元素的类型,枚举值BasicTypeesz:数组元素大小,以字节为单位该值因为被频繁查询,所以放在虚函数表指针的后面。
_super_check_offset:用于快速查找supertype的一个偏移量
_secondary_super_cache:Klass指针,上一次observed secondary supertype
_secondary_supers:Klass指针数组,指向secondary supertype,即类实现的接口对应的Kclass
_primary_supers:Klass指针数组,大小固定为8,指向primary supertypes,即默认继承的类如Object
_name: 类名,如java/lang/String,[Ljava/lang/String
_java_mirror: oopDesc指针,此类对应的java/lang/Class实例,可以据此访问类静态属性
_super:Klass指针,父类
_subklass:Klass指针,子类
_next_sibling:Klass指针,该类的下一个子类
_next_link:Klass指针,ClassLoader加载的下一个Klass
_class_loader_data :ClassLoaderData指针,加载该类的ClassLoader
_modifier_flags: 修改标识,Class.getModifiers使用
_access_flags:获取类的修饰符,如private类访问控制,final,static,abstract ,native等
测试用例如下:
package jvmTest; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; interface interTest{ void show(); } class Base{ private int a=1; public void print(){ System.out.println("Base"); } } class Base2 extends Base{ public int a; public Base2(int a) { this.a = a; } public void print(){ System.out.println("Base2"); } } class A extends Base2 implements interTest { public int b; public A(int a,int b) { super(a); this.b=b; } @Override public void show() { System.out.println("a->"+a+",b="+b); } public void print(){ System.out.println("A"); } } class B extends A{ private int c; public B(int a, int b) { super(a, b); c=3; } @Override public void show() { System.out.println("a->"+a+",b="+b+",c="+c); } public void print(){ System.out.println("B"); } } public class MainTest { public static void main(String[] args) { A a=new A(1,2); a.show(); A[] a2={a,new B(2,3)}; 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(); } }使用HSDB Class Browser查看jvmTest.A的Kclass,如下图:
使用Inspector可查看该Klass的各属性,如下图:
比如_super属性,无法直观的看出该属性对应的类是什么类,可以选中该行,然后复制出@后的地址,在Code Viewer中查询,如下图:
Klass定义了处理_layout_helper的各种工具方法,如layout_helper_is_instance,layout_helper_is_array等,定了处理_access_flag的各种工具方法,如is_abstract,is_interface等,定义跟Klass本身直接相关的方法,如is_subclass_of,is_subtype_of,find_field,lookup_method,verify,print_on等,多数是虚方法。
在哪去找Oop了?可以通过Stack Memory查看线程栈中局部变量所指向的Oop,也可通过scanoops搜索指定类型的Oop,拿到地址后,通过Inspect查看,以Stack Memory为例说明:
jvmTest.A的实例通过_metadata._compressed_klass属性保存了对jvmTest/A的InstanceKlass的引用,该实例有3个属性,分别是继承自Base的int a,继承自Base2的int a,jvmTest.A自己的int b。具体Oop的类定义和内存结构且听下回分解。
InstanceKlass是Java Class在JVM层面的内存表示,包含了类在执行过程中需要的所有信息。
InstanceKlass在Klass基础上新增的重要属性如下:
_annotations:Annotations指针,该类使用的所有注解
_array_klasses: 数组元素为该类的数组Klass指针
_constants: ConstantPool指针,该类的常量池
_inner_classes:用一个ushort数组表示当前类的内部类属性和闭包(EnclosingMethod)属性,参考Class类中的getEnclosingXX、getDeclaredXX
_array_name: 根据类名计算的以该类的数组的名字
_nonstatic_field_size:非静态字段的内存大小,以heapOopSize为单位,默认使用压缩指针时heapOopSize是int的大小
_static_field_size:静态字段的内存大小,以字宽(HeapWordSize,实际是一个指针变量的内存大小)为单位
_generic_signature_index:常量池中保存该类的Generic signature的索引
_source_file_name_index:包含此类的源文件名在常量池中索引
_static_oop_field_count:此类的包含的静态引用类型字段的数量
_java_fields_count:总的字段数量
_nonstatic_oop_map_size:非静态的oop map block的内存大小,以字宽为单位
_minor_version:此类的主版本号
_major_version:此类的次版本号
_init_thread:执行此类初始化的Thread指针
_vtable_len:Java 虚函数表(vtable)的内存大小,以字宽为单位
_itable_len:Java 接口函数表(itable)的内存大小,以字宽为单位
_oop_map_cache:OopMapCache指针,该类的所有方法的OopMapCache
_member_names: MemberNameTable指针,保存了成员名
_jni_ids:JNIid指针,该类的第一个静态字段的JNIid,可以根据其_next属性获取下一个字段的JNIid
_methods_jmethod_ids:jmethodID指针,java方法的ID列表
_dependencies:nmethodBucket指针,依赖的本地方法,以根据其_next属性获取下一个nmethod
_osr_nmethods_head:栈上替换的本地方法链表的头元素
_cached_class_file: class文件的内容,JVMTI retransform时使用
_idnum_allocated_count:已经分配的方法的idnum的个数,可以根据该ID找到对应的方法,如果JVMTI有新增的方法,已分配的ID不会变
_init_state:类的状态,是一个枚举值ClassState,allocated(已分配内存),loaded(从class文件读取加载到内存中),linked(已经成功链接和校验),being_initialized(正在初始化),fully_initialized(已经完成初始化),initialization_error(初始化异常)
_reference_type:引用类型
_methods:方法指针数组,类方法
_default_methods:方法指针数组,从接口继承的默认方法
_local_interfaces:Klass指针数组,直接实现的接口Klass
_transitive_interfaces:Klass指针数组,所有实现的接口Klass,包含_local_interfaces和通过继承间接实现的接口
_method_ordering:int数组,保存类中方法声明时的顺序,JVMTI使用
_default_vtable_indices:默认方法在虚函数表中的索引
_fields:类的字段属性,每个字段有6个属性,access, name index, sig index, initial value index, low_offset, high_offset,6个组成一个数组,access表示访问控制属性,根据name index可以获取属性名,根据initial value index可以获取初始值,根据low_offset, high_offset可以获取该属性在内存中的偏移量
接下来几个属性是内嵌的在类中的,没有对应的属性名,只能通过指针和偏移量的方式访问:
Java vtable:Java虚函数表,大小等于_vtable_len
Java itables:Java接口函数表,大小等于 _itable_len
非静态oop-map blocks ,大小等于_nonstatic_oop_map_size
接口的实现类,仅当前类表示一个接口时存在,如果接口没有任何实现类则为NULL,如果只有一个实现类则为该实现类的Klass指针,如果有多个实现类,为当前类本身
host klass,只在匿名类中存在,为了支持JSR 292中的动态语言特性,会给匿名类生成一个host klass。
测试用例:
package jvmTest; import javax.xml.bind.annotation.XmlElement; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; interface interTest { void show(); } interface interTest2 { void show2(); } class Base { private int a = 1; public void print() { System.out.println("Base"); } } class Base2 extends Base implements interTest2 { public int a; public Base2(int a) { this.a = a; } @Override public void print() { System.out.println("Base2"); } @Override public void show2() { System.out.println("show2 Base2 "); } } class A extends Base2 implements interTest { @XmlElement public int b; public static int si = 10; public static String ss = "test"; public A(int a, int b) { super(a); this.b = b; } @Override public void show() { System.out.println("a->" + a + ",b=" + b); } @Override public void print() { System.out.println("A"); } public void print2() { System.out.println("A2"); } } class B extends A { private int c; public B(int a, int b) { super(a, b); c = 3; } @Override public void show() { System.out.println("a->" + a + ",b=" + b + ",c=" + c); } @Override public void print() { System.out.println("B"); } } public class MainTest { public static void main(String[] args) { A a = new A(1, 2); a.show(); A[] a2 = {a, new B(2, 3)}; 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(); } }在Inspector界面查找到_methods属性如下图:
Array对象的_data属性是一个T[],_data的地址就是头元素的地址,如下图:
可以用mem查看剩下的几个元素的地址,如下图:
与Code View界面查看的方法地址一致,如下图:
对应的_idnum_allocated_count的属性也是5,即总共5个方法,如下图:
_local_interfaces属性的长度为1,如下图:
_data[0]对应的Kclass就是interTest,如下图:
_transitive_interfaces属性的长度为2,如下图:
使用mem查看两个Klass的地址,如下图:
第二个就是interTest,第一个对应的Klass如下图:
interTest2不是jvmTest.A直接实现的接口,而是通过继承Base2间接实现的接口。
_fields字段记录了所有字段的相关属性,单个field的各属性用一个类FieldInfo表示,在fieldInfo.hpp中定义,FieldInfo本身也是用一个u2数组来保存各属性,并定义了一个枚举来对应数组各索引的具体含义,如下图:
根据这些offset计算属性的属性名或者初始化值,逻辑较为复杂,不能直观的判断,这里不做探讨。
_java_fields_count的属性为3,jvmTest.A一共三个字段,b,si,ss,如下:
_source_file_name_index的值为42,可以查看常量池中42对应的选项,如下图:
_nonstatic_field_size大小为3,单位是heapOopSize,开启指针压缩时跟int大小一样,因为有Base和Base2各有一个int,A有一个int,总共3个。_static_field_size的大小1,单位是HeapWord,跟指针大小一样,在64位CPU下是8字节,A的static变量有两个,si是4字节,ss开启指针压缩后也是4字节,加起来是8字节。_static_oop_field_count为1,A只有一个静态的String类型的字段ss。
heapOopSize的定义在globalDefinitions.hpp中,如下图:
HeapWord的定义也在 globalDefinitions.hpp中,如下图:
InstanceKlass除定义了与上述属性相关的方法外,还定义以下几类方法:
获取类方法相关的,如method_with_idnum,find_method,find_method_by_name,find_method_impl等获取字段属性相关的,如field_name, find_field, find_interface_field等类加载相关的,如link_class_impl,verify_code,initialize_impl等用于遍历oop,垃圾回收使用的oop_oop_iterate系列方法,如oop_oop_iterate_nv(oop,G1ParScanClosure)根据Klass创建Oop的方法,如allocate_instance,allocate_objArray等Method用于表示一个Java方法,因为一个应用有成千上万个方法,因此保证Method类在内存中短小非常有必要。为了本地GC方便,Method把所有的指针变量和方法大小放在Method内存布局的前面,方法本身的不可变数据如字节码用ConstMethod表示,可变数据如Profile统计的性能数据等用MethodData表示,都通过指针访问。如果是本地方法,Method内存结构的最后是native_function和signature_handler,按照解释器的要求,这两个必须在固定的偏移处。Method没有子类,定义在method.hpp文件中,其类继承关系如下图:
Method包含的属性如下:
_constMethod:ConstMethod指针,该类定义constMethod.hpp中,用于表示方法的不可变的部分,如方法ID,方法的字节码大小,方法名在常量池中的索引等,注意其中_constMethod_size的单位为字宽,_code_size的单位是字节,其内存结构如下图,因为异常检查表平均长度小于2,本地变量表大多数情况下没有,所以这两个没有被压缩保存。访问这些内嵌表都很快,不是性能瓶颈。ConstMethod提供了获取内嵌部分如字节码的起始地址,然后可以据此方法字节码了。
_method_data:MethodData指针,该类在methodData.hpp中定义,用于表示一个方法在执行期间收集的相关信息,如方法的调用次数,在C1编译期间代码循环和阻塞的次数,Profile收集的方法性能相关的数据等。
_method_counters:MethodCounters指针,该类在methodCounters.hpp中定义,用于记录方法调用次数,方法抛出异常的次数,方法断点的个数,主要用于基于调用频率的热点方法的跟踪统计。
_access_flags:AccessFlags类,表示方法的访问控制标识
_vtable_index: 该方法在vtable表中的索引
_method_size:这个Method对象的大小,以字宽为单位
_compiled_invocation_count:被编译成本地方法后调用的次数
_i2i_entry:解释器的入口地址
_adapter:此方法在解释器和编译器执行的适配器
_from_compiled_entry:执行编译后的代码的入口地址
_code:nmethod类指针,表示该方法编译后的本地代码
_from_interpreted_entry:code ? _adapter->i2c_entry() : _i2i_entry的缓存
以上一节的示例中的print2()方法为例进行分析,Class Brower中找到该方法的地址,然后在Inspector中查看,如下图:
其中_name_index即方法名在常量池的索引是39,方法签名在常量池的索引是37,常量池对应的数据如下图:
方法签名中()表示方法入参,V表示方法无返回值。
_max_locals即方法栈帧中本地变量的最大个数,为1,因为方法中只有一个本地变量,字符串A2, _max_stack即方法栈帧的对象的最大个数,为2,这个是方法的字节码决定的,第一步是获取System的out对象将其入栈,第二步是将字符串A2入栈,第三步将已入栈的两个对象作为入参调用println方法,所以栈帧的最大深度是2,
使用inspect命令查看print2方法的地址,如下图:
size是88,这个size是用sizeof算出来的,单位是字节,_method_size是11,单位是字段,8字节,两者一致。
接着用mem命令查看具体的内存数据,如下图:
第一个8字节是kclass header,第二个8字节就是属性_constMethod的值了,第三和第四个8字节都是0,即空指针,对应methodData属性和methodCounters属性,两者都是空指针;第五个8字节分别是_vtable_index,取值是8和_access_flags,取值是1;第六个8字节分别是_intrinsic_id和_method_size,前者取值0,后者取值11;第七个8字节是_i2i_entry的地址,第八个8字节是_adapter的地址,跟inspect的结果一致;第九个8字节是_from_compiled_entry的地址,第十个8字节是_code的地址,空指针;第十一个8字节是_from_interpreted_entry的地址。
再看ConstMethod的内存结构,用printas和mem命令查看,如下图:
ConstMethod的内存大小是48字节,但是_constMethod_size是10*8=80字节,多出来的32字节就是内嵌到ConstMethod中的字节码,代码行号表,异常检查表等,这部分没有对应的属性所以sizeof没有统计这部分对应的内存。
第一个8字节是_fingerprint, 第二个8字节是常量池指针_constants,第三个8字节是空指针_stackmap_data,第四个8字节是属性_constMethod_size,取值10,占后4字节,前面2字节是_flags,取值5,开始的2字节是填充的;第五个8字节分别是属性_method_idnum,取值4,_signature_index,取值37,_name_index,取值39,_code_size,取值9,给占2字节;第6个8字节分别是属性_method_idnum,取值4,_max_locals,取值1,_size_of_parameters,取值1,_max_stack取值2,至此ConstMethod的所有属性都有对应的内存区域,刚好48字节。
ConstMethod的第十个8字节的起始地址是0x0000000016c539d0,下一个8字节的起始地址是0x0000000016c539d8,刚好是Method的起始地址,说明这两者在内存中是紧挨着的。
C++中的vtable只包含虚函数,非虚函数在编译期就已经解析出正确的方法调用了。Java vtable除了虚方法外还包含了其他的非虚方法。vtable中的一条记录用vtableEntry表示,该类在klassVtable.hpp中定义,该类只有一个属性Method* _method,只是对Method*做了简单包装而已,提供了相关的便利方法。访问vtable需要通过klassVtable类,该类也是在klassVtable.hpp中定义,提供了访问vtable中的方法的便利方法,如Method* method_at(int i),int index_of(Method* m)等,其实现都是基于vtable的内存起始地址和内存偏移来完成的。
klassVtable包含三个属性,分别是_klass(该vtable所属的klass),_tableOffset(vtable在klass实例内存中的偏移量),_length(vtable的长度,即vtableEntry的条数,因为一个vtableEntry实例只包含一个Method*,其大小等于字段,所以vtable的长度跟vtable以字宽为单位的内存大小相同),_verify_count(用于记录vtable是否已经校验并初始化完成)如下图:
继续以InstanceKlass中的示例代码说明,使用inspect命令查看jvmTest.A的内存大小为440字节,即55字宽,如下图:
在440字节之后就是vtable了,其长度是9,用 mem 0x000000013f541240 56查看440字节之后的起始地址为0x000000013f5413f8,然后查看该地址之后9字宽的内容,如下图:
可以在Code Viewer中查看这9个方法地址对应的方法实现,如下图:
9个地址对应的方法如下:
Java itable是Java接口函数表,为了方便查找某个接口对应的方法实现。itable的结构比vtable复杂,除了记录方法地址外还得记录该方法所属的接口类klass地址,其中方法地址用itableMethodEntry表示,跟vtableEntry一样,只包含了一个Method* _method属性,方法所属的接口类klass地址用itableOffsetEntry表示,包含两个属性Klass* _interface(该方法所属的接口)和int _offset(该接口下的第一个方法itableMethodEntry相对于所属Klass的偏移量)。itable的内存布局如下:
这两者都是成对出现的。访问itable通过类klassItable实现,该类包含4个属性:_klass(itable所属的Klass),_table_offset(itable在所属Klass中的内存偏移量),_size_offset_table(itable中itableOffsetEntry的条数),_size_method_table(itable中itableMethodEntry的条数),如下图:
通过mem查看itable 8个字段的内存,如下图:
用Code Viewer可以查看各地址对应的接口类和方法,如下图:
其中 0x000000013f541470 减去 A的起始地址 0x000000013f541240,刚好就是偏移量0x230,即相对于vtable偏移15个字宽,vtable本身占9个字宽,加上x000000013f541470前面的6个字宽,刚好15个字宽。
InstanceKlass一共有3个特殊子类,如下:
InstanceClassLoaderKlass,没有添加新的字段,增加了新的oop遍历方法,主要用于类加载器依赖遍历使用。InstanceRefKlass,用于表示java/lang/ref/Reference的子类,这些类需要垃圾回收器特殊处理,因此改写了原有的oop_oop_iterate中用于垃圾回收的相关方法。InstanceMirrorKlass,用于表示特殊的java.lang.Class类,java.lang.Class对应的OopDesc实例用于保存类的静态属性,因此他们的实例大小不同,需要特殊的方式来计算他们的大小以及属性遍历。Klass的属性_java_mirror就指向保存该类静态字段的OopDesc实例,可通过该属性访问类的静态字段。将jvmTest.A的静态属性改成4个int变量,如下图:
通过HSDB可以查看_java_mirror属性,如下图:
ArrayKlass继承自Klass,是所有数组类的抽象基类,在Klass的基础上增加如下属性:
_dimension:int类型,表示数组的维度,记为n_higher_dimension:Klass指针,表示对n+1维数组Klass的引用_lower_dimension: Klass指针,表示对n-1维数组Klass的引用_vtable_len: int类型, 虚函数表的长度_component_mirror:oop, 数组元素对应的java/lang/Class的Oop该类的方法不多,主要是跟属性相关的方法,比较重要的就是两个分配数组内存的方法multi_allocate和allocate_arrayArray。
ObjArrayKlass是ArrayKlass的子类,用于表示元素是类的数组或者多维数组,该类新增了两个属性:
_element_klass:Klass指针,数组元素对应的Klass引用,如果是多维数组,则是对应数组元素的ObjArrayKlass的引用 _bottom_klass:一维数组的类型,可以是InstanceKlass或者TypeArrayKlass该类主要增加了两个用于数组复制的重要方法,copy_array和do_copy,以及用于垃圾回收的oop_oop_iterate的相关方法。
TypeArrayKlass是ArrayKlass的子类,用于表示元素是基本类型如int的数组,该类新增一个属性:
_max_length:该数组的最大长度同ObjArrayKlass也增加了数组复制方法copy_array和用于垃圾回收的oop_oop_iterate的相关方法。
测试用例如下:
package jvmTest; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; class C{ private static int a=1; private int b=2; public C(int b) { this.b = b; } } public class MainTest2 { public static void main(String[] args) { C[] c={new C(1)}; C[][] c2={{new C(2),new C(3)},{new C(4)}}; int[] i={1}; int[][] i2={{1,2},{3,4,5}}; 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(); } }因为数组不是一个单独的class,所以无法在Class Browser中查看,只能在Stack Memery中查看线程栈中4个数组变量所引用的ArrayKlass对应的OopDesc来查看,如下图:
4个局部数组变量加上main方法的入参args,该参数是String数组,刚好是5个,这5个变量的顺序是按照线程栈入栈的顺序来的,最下面的是args,从下到上依次是c,c2,i,i2。用Inspect查看该地址对应的数据,i2如下图:
c2的如下图:
上图可知,基本类型数组用TypeArrayKlass表示,类数组或者多维数组用ObjArrayKlass表示,可将对应的TypeArrayKlass或者ObjArrayKlass展开查看对应的属性。