美文网首页
Hotspot Klass模型

Hotspot Klass模型

作者: 程序员札记 | 来源:发表于2022-05-16 08:45 被阅读0次

当创建一个对象的时候,你有没有发现新生区和元数据区内存占用都有所增加呢?而这和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* 的别名,如下图:

image.png

Java对象在内存中是实例数据和类型数据相分离的,实例数据保存了一个指向类型数据的指针,即OOP(ordinary object pointer),因此猜测这里的Klass就是所谓的类型数据,oopDesc就是具体的实例数据了。

1、类继承结构
在上述代码Klass处按crtl并点击即可进入到定义Klass的头文件kclass.hpp中,该文件的位于hotspot\src\share\vm\oops目录下,选中Klass,点击右键,选择Open Type Hierarchy即可显示该类的继承关系图了,如下:

image.png

选择其中某一个类如MetaspaceObj,右键选择Open即可进入定义该父类的头文件alloction.hpp,位于hotspot\src\share\vm\memory下,如下图:

image.png

2、MetaspaceObj
该类是作为存放在Metaspace元空间的中类的基类,不能执行delete,否则会出现致命错误,注意该类没有定义虚函数。该类重载了new操作符,主要用于给共享只读的或者共享读写的类分配内存,该类定义了如下方法:

image.png

该类定义了一个枚举Type用于表示对象类型,包含以下几种类型:


image.png

3、Metadata
Metadata是内部表示类相关元数据的一个基类,注意Metadata定义了多个虚函数,其定义的方法如下:

image.png

其中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.png

interTest2不是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.png

heapOopSize的定义在globalDefinitions.hpp中,如下图:


image.png

HeapWord的定义也在 globalDefinitions.hpp中,如下图:

image.png

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等

6、Method
Method用于表示一个Java方法,因为一个应用有成千上万个方法,因此保证Method类在内存中短小非常有必要。为了本地GC方便,Method把所有的指针变量和方法大小放在Method内存布局的前面,方法本身的不可变数据如字节码用ConstMethod表示,可变数据如Profile统计的性能数据等用MethodData表示,都通过指针访问。如果是本地方法,Method内存结构的最后是native_function和signature_handler,按照解释器的要求,这两个必须在固定的偏移处。Method没有子类,定义在method.hpp文件中,其类继承关系如下图:

image.png

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.png

size是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.png

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的起始地址,说明这两者在内存中是紧挨着的。

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.png

4个局部数组变量加上main方法的入参args,该参数是String数组,刚好是5个,这5个变量的顺序是按照线程栈入栈的顺序来的,最下面的是args,从下到上依次是c,c2,i,i2。用Inspect查看该地址对应的数据,i2如下图:

image.png

c2的如下图:

[图片上传中...(image.png-b5ef8-1652661843802-0)]

上图可知,基本类型数组用TypeArrayKlass表示,类数组或者多维数组用ObjArrayKlass表示,可将对应的TypeArrayKlass或者ObjArrayKlass展开查看对应的属性。

相关文章

  • 类模型

    HotSpot采用了OOP-Klass模型描述Java的类和对象。Klass模型采用Klass类及相关子类的对象来...

  • Hotspot Klass模型

    当创建一个对象的时候,你有没有发现新生区和元数据区内存占用都有所增加呢?而这和OOP-Klass二分模型有关。 O...

  • C++OOP对象的内存布局

    HotSpot采用了OOP-Klass模型来描述Java类和对象。OOP(Ordinary Object Poin...

  • Java对象模型

    oop-klass模型 Hotspot 虚拟机在内部使用两组类来表示Java的类和对象。 oop(ordinary...

  • klass模型

    学java的都知道,我们的代码是由一个个类文件组成,类文件编译成class文件放在jvm中去运行。那么class文...

  • synchronized 的实现

    在 Hotspot 虚拟机中,Java 对象头包含_mark 和_klass(继承自 OopDesc),其中syn...

  • JVM系列-05-方法区-永久带VS元空间

    [TOC] 1 JVM(HotSpot)内存模型 上面这种图是HotSpot虚拟机的内存模型,本篇文章主要介绍方法...

  • JVM源码实战 - OOP-Klass模型

    Github原文链接 1 OOP-Klass(Ordinary Object Pointer)模型 OOP-Kla...

  • JVM底层类加载

    Klass模型 Java中的每个类,在JVM中都有对应的Klass类实例与之对应,储存类的元信息如:常量池、属性信...

  • 类加载器子系统之类加载(一)

    一、klass模型 Java的每个类,在JVM中,都有一个对应的Klass类实例与之对应,存储类的元信息如:常量池...

网友评论

      本文标题:Hotspot Klass模型

      本文链接:https://www.haomeiwen.com/subject/mcpiurtx.html