Hotspot Klass模型

2022-05-16  本文已影响0人  程序员札记

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

OOP-Klass二分模型介绍

HotSpot中采用了OOP-Klass模型,它是用来描述Java对象实例的一种模型

在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类型共享的结构和行为,描述类型自身的布局,以及与其他类之间的关系(父类、子类、兄弟类等)

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作为父类主要职责是描述了类的继承关系, 其包含的重要属性如下:

该值因为被频繁查询,所以放在虚函数表指针的后面。

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基础上新增的重要属性如下:

接下来几个属性是内嵌的在类中的,没有对应的属性名,只能通过指针和偏移量的方式访问:

接口的实现类,仅当前类表示一个接口时存在,如果接口没有任何实现类则为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除定义了与上述属性相关的方法外,还定义以下几类方法:

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

以上一节的示例中的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个特殊子类,如下:

将jvmTest.A的静态属性改成4个int变量,如下图:


image.png

通过HSDB可以查看_java_mirror属性,如下图:


image.png

10、ArrayKlass
ArrayKlass继承自Klass,是所有数组类的抽象基类,在Klass的基础上增加如下属性:

该类的方法不多,主要是跟属性相关的方法,比较重要的就是两个分配数组内存的方法multi_allocate和allocate_arrayArray。

ObjArrayKlass是ArrayKlass的子类,用于表示元素是类的数组或者多维数组,该类新增了两个属性:

TypeArrayKlass是ArrayKlass的子类,用于表示元素是基本类型如int的数组,该类新增一个属性:

测试用例如下:

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展开查看对应的属性。

上一篇 下一篇

猜你喜欢

热点阅读