Hotspot Klass模型
当创建一个对象的时候,你有没有发现新生区和元数据区内存占用都有所增加呢?而这和OOP-Klass二分模型有关。
OOP-Klass二分模型介绍
HotSpot中采用了OOP-Klass模型,它是用来描述Java对象实例的一种模型
- OOP或OOPS(Ordinary Object Pointer)指的是普通对象指针,主要职能是表示对象的实例数据,存储在堆里面
- Klass用来描述对象实例的具体类型,实现语言层面的Java类,存储在元空间(方法区)
OOP
在Java应用程序运行过程中,每创建一个Java对象,在JVM内部也会相应创建一个OOP对象来表示Java对象。OOP类的共同基类型是oopDesc
根据JVM内部使用的对象业务类型,具有多种oopDesc子类,比如instanceOopDesc表示类实例,arrayOopDesc表示数组。
其中,instanceOopDesc和arrayOopDesc又称为对象头,instanceOopDesc对象头包括以下两部分信息:Mark Word 和 元数据指针(Klass*):
Mark Word,主要存储对象运行时记录信息,如hashcode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
元数据指针,instanceOopDesc中的_metadata成员,它是联合体,可以表示未压缩的Klass指针(_klass)和压缩的Klass指针。对应的klass指针指向一个存储类的元数据的Klass对象
oopDesc.hpp
//hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
//....
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
//....
}
oopDesc中包含两个数据成员:_mark 和 _metadata。其中markOop类型的_mark对象指的是前面讲到的Mark World。_metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针,它们就是前面讲到的元数据指针,这两个指针都指向instanceKlass对象,它用来描述对象的具体类型。
Klass与instanceKlass
Klass数据结构定义类所有Klass类型共享的结构和行为,描述类型自身的布局,以及与其他类之间的关系(父类、子类、兄弟类等)
- _layout_helper:描述对象整体布局
- _name:表示类名
- _java_mirror:表示Klass的Java层镜像类
- _super:表示父类
- _subklass:表示第一个子类
- next_slibling:指向的是下一个兄弟节点,JVM通过_subklass->next_slibling可以找到下一个子类
在HotSpot中,为每一个已加载的Java类创建一个instanceKlass对象,用来标识Java内部类型的机制。instanceKlass对象的所有成员可以包含JVM内部运行一个Java类所需的所有信息,这些成员变量在类解析阶段完成赋值。
instanceKlass.hpp
class instanceKlass: public Klass {
friend class VMStructs;
public:
enum ClassState {
unparsable_by_gc = 0, // object is not yet parsable by gc. Value of _init_state at object allocation.
allocated, // allocated (but not yet linked)
loaded, // loaded and inserted in class hierarchy (but not linked yet)
linked, // successfully linked/verified (but not initialized yet)
being_initialized, // currently running class initializer
fully_initialized, // initialized (successfull final state)
initialization_error // error happened during initialization
};
//部分内容省略
protected:
// Method array. 方法数组
objArrayOop _methods;
// Interface (klassOops) this class declares locally to implement.
objArrayOop _local_interfaces; //该类声明要实现的接口.
// Instance and static variable information
typeArrayOop _fields;
// Constant pool for this class.
constantPoolOop _constants; //常量池
// Class loader used to load this class, NULL if VM loader used.
oop _class_loader; //类加载器
typeArrayOop _inner_classes; //内部类
Symbol* _source_file_name; //源文件名
}
其中,ClassState描述了类加载的状态:分配、加载、链接、初始化。
instanceKlass的布局包括:声明接口、字段、方法、常量池、源文件名等等
image.png
通过OOP-Klass模型,就可以分析出Java虚拟机是如何通过栈帧中的对象引用找到对应的对象实例。
image.png
从图中可以看出,通过栈帧中的对象引用找到Java堆中的instanceOop对象,当需要调用对象方法或访问类变量,可以再通过instanceOop中持有的类元数据指针来找到方法区中的instanceKlass对象来完成。
klass和oop之间的联系
image.png当我们执行new Object()的时候,首先JVM native层判断该类是否被加载过,没有的话就进行类的加载,并在JVM内部创建一个instanceKlass对象表示该类的运行时元数据(Java层的Class对象),到初始化的时候,JVM就创建一个instanceOopDesc对象表示该对象的实例,然后进行Mark Word信息填充,将元数据指针指向Klass对象,并填充实例变量。
代码
在分析thread.cpp的create_vm函数中,发现JVM通过initialize_class函数来加载Java类,该函数是threap.cpp的一个静态函数,其函数定义如下:
![image.png](https://img.haomeiwen.com/i26273155/6f0f59cc26b3b3f7.png?imageMo
接着为main_thread创建thread_object即Java中的java.lang.Thread对象时使用了create_initial_thread函数,该函数返回了oop,实际是类oopDesc* 的别名,如下图:
Java对象在内存中是实例数据和类型数据相分离的,实例数据保存了一个指向类型数据的指针,即OOP(ordinary object pointer),因此猜测这里的Klass就是所谓的类型数据,oopDesc就是具体的实例数据了。
1、类继承结构
在上述代码Klass处按crtl并点击即可进入到定义Klass的头文件kclass.hpp中,该文件的位于hotspot\src\share\vm\oops目录下,选中Klass,点击右键,选择Open Type Hierarchy即可显示该类的继承关系图了,如下:
选择其中某一个类如MetaspaceObj,右键选择Open即可进入定义该父类的头文件alloction.hpp,位于hotspot\src\share\vm\memory下,如下图:
image.png2、MetaspaceObj
该类是作为存放在Metaspace元空间的中类的基类,不能执行delete,否则会出现致命错误,注意该类没有定义虚函数。该类重载了new操作符,主要用于给共享只读的或者共享读写的类分配内存,该类定义了如下方法:
该类定义了一个枚举Type用于表示对象类型,包含以下几种类型:
image.png
3、Metadata
Metadata是内部表示类相关元数据的一个基类,注意Metadata定义了多个虚函数,其定义的方法如下:
其中identity_hash()方法返回的实际是该对象的内存地址,如下图:
其中跟stack相关的三个方法是类重定义(class redefinition)期间使用的,跟Java栈帧无关。
4、Klass
一个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,否则是0xC0
- hsz: 数组头元素的字节数
- ebt:数组元素的类型,枚举值BasicType
- esz:数组元素大小,以字节为单位
该值因为被频繁查询,所以放在虚函数表指针的后面。
-
_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();
}
}
要看JVM里的数据结构,介绍一个很强大的jdk自带工具HSDB,使用HSDB Class Browser查看jvmTest.A的Kclass,如下图:
image.png使用Inspector可查看该Klass的各属性,如下图:
image.png比如_super属性,无法直观的看出该属性对应的类是什么类,可以选中该行,然后复制出@后的地址,在Code Viewer中查询,如下图:
image.png 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为例说明:
image.png
jvmTest.A的实例通过_metadata._compressed_klass属性保存了对jvmTest/A的InstanceKlass的引用,该实例有3个属性,分别是继承自Base的int a,继承自Base2的int a,jvmTest.A自己的int b。具体Oop的类定义和内存结构且听下回分解。
5、InstanceKlass
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属性如下图:
image.png
Array对象的_data属性是一个T[],_data的地址就是头元素的地址,如下图:
image.png可以用mem查看剩下的几个元素的地址,如下图:
image.png与Code View界面查看的方法地址一致,如下图:
[图片上传中...(image.png-2ff3c-1652661158342-0)]
对应的_idnum_allocated_count的属性也是5,即总共5个方法,如下图:
image.png
_local_interfaces属性的长度为1,如下图:
image.png_data[0]对应的Kclass就是interTest,如下图:
image.png
_transitive_interfaces属性的长度为2,如下图:
image.png使用mem查看两个Klass的地址,如下图:
image.png
第二个就是interTest,第一个对应的Klass如下图:
image.pnginterTest2不是jvmTest.A直接实现的接口,而是通过继承Base2间接实现的接口。
_fields字段记录了所有字段的相关属性,单个field的各属性用一个类FieldInfo表示,在fieldInfo.hpp中定义,FieldInfo本身也是用一个u2数组来保存各属性,并定义了一个枚举来对应数组各索引的具体含义,如下图:
image.png根据这些offset计算属性的属性名或者初始化值,逻辑较为复杂,不能直观的判断,这里不做探讨。
_java_fields_count的属性为3,jvmTest.A一共三个字段,b,si,ss,如下:
image.png_source_file_name_index的值为42,可以查看常量池中42对应的选项,如下图:
image.png_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。
image.pngheapOopSize的定义在globalDefinitions.hpp中,如下图:
image.png
HeapWord的定义也在 globalDefinitions.hpp中,如下图:
image.pngInstanceKlass除定义了与上述属性相关的方法外,还定义以下几类方法:
- 获取类方法相关的,如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等
6、Method
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提供了获取内嵌部分如字节码的起始地址,然后可以据此方法字节码了。
image.png
-
_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中查看,如下图:
image.png其中_name_index即方法名在常量池的索引是39,方法签名在常量池的索引是37,常量池对应的数据如下图:
image.png方法签名中()表示方法入参,V表示方法无返回值。
_max_locals即方法栈帧中本地变量的最大个数,为1,因为方法中只有一个本地变量,字符串A2, _max_stack即方法栈帧的对象的最大个数,为2,这个是方法的字节码决定的,第一步是获取System的out对象将其入栈,第二步是将字符串A2入栈,第三步将已入栈的两个对象作为入参调用println方法,所以栈帧的最大深度是2,
image.png使用inspect命令查看print2方法的地址,如下图:
image.pngsize是88,这个size是用sizeof算出来的,单位是字节,_method_size是11,单位是字段,8字节,两者一致。
接着用mem命令查看具体的内存数据,如下图:
image.png
第一个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命令查看,如下图:
image.pngConstMethod的内存大小是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的起始地址,说明这两者在内存中是紧挨着的。
7、Java vtable
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是否已经校验并初始化完成)如下图:
image.png继续以InstanceKlass中的示例代码说明,使用inspect命令查看jvmTest.A的内存大小为440字节,即55字宽,如下图:
image.png在440字节之后就是vtable了,其长度是9,用 mem 0x000000013f541240 56查看440字节之后的起始地址为0x000000013f5413f8,然后查看该地址之后9字宽的内容,如下图:
image.png可以在Code Viewer中查看这9个方法地址对应的方法实现,如下图:
image.png
9个地址对应的方法如下:
image.png
8、Java itable
Java itable是Java接口函数表,为了方便查找某个接口对应的方法实现。itable的结构比vtable复杂,除了记录方法地址外还得记录该方法所属的接口类klass地址,其中方法地址用itableMethodEntry表示,跟vtableEntry一样,只包含了一个Method* _method属性,方法所属的接口类klass地址用itableOffsetEntry表示,包含两个属性Klass* _interface(该方法所属的接口)和int _offset(该接口下的第一个方法itableMethodEntry相对于所属Klass的偏移量)。itable的内存布局如下:
image.png
这两者都是成对出现的。访问itable通过类klassItable实现,该类包含4个属性:_klass(itable所属的Klass),_table_offset(itable在所属Klass中的内存偏移量),_size_offset_table(itable中itableOffsetEntry的条数),_size_method_table(itable中itableMethodEntry的条数),如下图:
image.png
通过mem查看itable 8个字段的内存,如下图:
image.png
用Code Viewer可以查看各地址对应的接口类和方法,如下图:
image.png其中 0x000000013f541470 减去 A的起始地址 0x000000013f541240,刚好就是偏移量0x230,即相对于vtable偏移15个字宽,vtable本身占9个字宽,加上x000000013f541470前面的6个字宽,刚好15个字宽。
9、InstanceKlass特殊子类
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变量,如下图:
image.png
通过HSDB可以查看_java_mirror属性,如下图:
image.png
10、ArrayKlass
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来查看,如下图:
image.png4个局部数组变量加上main方法的入参args,该参数是String数组,刚好是5个,这5个变量的顺序是按照线程栈入栈的顺序来的,最下面的是args,从下到上依次是c,c2,i,i2。用Inspect查看该地址对应的数据,i2如下图:
image.pngc2的如下图:
[图片上传中...(image.png-b5ef8-1652661843802-0)]
上图可知,基本类型数组用TypeArrayKlass表示,类数组或者多维数组用ObjArrayKlass表示,可将对应的TypeArrayKlass或者ObjArrayKlass展开查看对应的属性。