专注虚拟机与编译器研究

第2.2篇-HotSpot VM源码分析之类模型

HotSpot VM采用了OOP-Klass模型描述Java的类和对象。Klass模型采用Klass类及相关子类的对象来描述具体的Java类。一般HotSpot VM 在加载Java的Class 文件时,会在方法区创建Klass实例,这个实例用来保存Java类的元数据,包括常量池、字段、方法等。

Klass模型中相关类的继承体系如下图所示。

  

Metadata是元数据类的基础类型,除了Klass会直接继承外,表示方法的Method与表示常量池的ConstantPool也会继承,这里只讨论Klass继承体系中涉及到的相关类。

整个Klass模型中涉及到的C++类主要提供了2个功能: 

(1)提供C++层面的Java类型(包括Java类和Java数组)表示,也就是用C++类的实例来描述Java类型;

(2)Java方法动态分派。

这一篇文章重点介绍一下Klass这个基础类型。

一个Klass实例(注意是Klass实例表示Java类的元数据,所以不同的Java类就用不同的Klass实例表示)代表一个Java类的元数据(相当于java.lang.Class对象)。所以Klass中要有描述Java类中常量池、字段、方法的能力,也就是能保存这些信息,同时还能提供一些函数供HotSpot VM的开发者操作这些信息。

下面重点介绍一下Klass类。

一个C++类Klass的实例表示一个Java类型的元数据(相当于java.lang.Class对象),主要提供了两个功能:

  • 实现Java语言层面的类型

  • 提供Java多态方法的支持

在HotSpot VM中,Java对象使用oop实例来表示,不提供任何虚函数功能,oop实例保存了对应Klass的指针,所有方法调用通过Klass完成并通过Klass获取类型信息,Klass基于C++的虚函数提供对Java多态的支持。

笔者一般在描述Java类型的对象时使用“对象”这个术语,描述C++类的对象时使用“实例”这个术语,同样,描述Java的方法时使用“方法”术语,描述C++的方法时使用“函数”术语。在描述C++类时,使用属性,而在描述Java类时使用字段。

Klass类及重要属性的定义如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.hpp

class Klass : public Metadata {
 // ...
 protected:
  // note: put frequently-used fields together at start of klass structure
  // for better cache behavior (may not make much of a difference but sure won't hurt)
  enum { _primary_super_limit = 8 };
 
  // The "layout helper" is a combined descriptor of object layout.
  // For klasses which are neither instance nor array, the value is zero.
  //
  // For instances, layout helper is a positive number, the instance size.
  // This size is already passed through align_object_size and scaled to bytes.
  // The low order bit is set if instances of this class cannot be
  // allocated using the fastpath.
  //
  // For arrays, layout helper is a negative number, containing four
  // distinct bytes, as follows:
  //    MSB:[tag, hsz, ebt, log2(esz)]:LSB
  // where:
  //    tag is 0x80 if the elements are oops, 0xC0 if non-oops
  //    hsz is array header size in bytes (i.e., offset of first element)
  //    ebt is the BasicType of the elements
  //    esz is the element size in bytes
  // This packed word is arranged so as to be quickly unpacked by the
  // various fast paths that use the various subfields.
  //
  // The esz bits can be used directly by a SLL instruction, without masking.
  //
  // Note that the array-kind tag looks like 0x00 for instance klasses,
  // since their length in bytes is always less than 24Mb.
  //
  // Final note:  This comes first, immediately after C++ vtable,
  // because it is frequently queried.
  jint        _layout_helper;
 
  // The fields _super_check_offset, _secondary_super_cache, _secondary_supers
  // and _primary_supers all help make fast subtype checks.  See big discussion
  // in doc/server_compiler/checktype.txt
  //
  // Where to look to observe a supertype (it is &_secondary_super_cache for
  // secondary supers, else is &_primary_supers[depth()].
  juint       _super_check_offset;
 
  // Class name.  Instance classes: java/lang/String, etc.  Array classes: [I,
  // [Ljava/lang/String;, etc.  Set to zero for all other kinds of classes.
  Symbol*     _name;
 
  // Cache of last observed secondary supertype
  Klass*      _secondary_super_cache;
  // Array of all secondary supertypes
  Array<Klass*>* _secondary_supers;
  // Ordered list of all primary supertypes
  Klass*      _primary_supers[_primary_super_limit];
  // java/lang/Class instance mirroring this class
  oop         _java_mirror;
  // Superclass
  Klass*      _super;
  // First subclass (NULL if none); _subklass->next_sibling() is next one
  Klass*      _subklass;
  // Sibling link (or NULL); links all subklasses of a klass
  Klass*      _next_sibling;
 
  // All klasses loaded by a class loader are chained through these links
  Klass*      _next_link;
 
  // The VM's representation of the ClassLoader used to load this class.
  // Provide access the corresponding instance java.lang.ClassLoader.
  ClassLoaderData* _class_loader_data;
 
  // Access flags. The class/interface distinction is stored here.
  AccessFlags _access_flags;    
 
 // Used when biased locking is both enabled and disabled for this type
  markOop  _prototype_header;  
  
  ...
}

下表对各个属性进行了简单的介绍。 

字段名 作用
_layout_helper

对象布局的综合描述符。如果不是InstanceKlass或ArrayKlass,值为0。如果是InstantceKlass或

ArrayKlass时,这个值是个组合数字。

(1)对于InstanceKlass而言,组合数字中包含有表示对象的、以字节为单位的内存占用大小,也就是说InstanceKlass实例表示Java类,由这个Java类创建的对象所需要的内存大小。

(2)对于ArrayKlass而言,该值是一个组合数字,包含4部分,具体怎么组合和解析由子类实现:

  • tag:如果数组元素的类型为对象类型,值为0x80;否则值为0xC0,表示数组元素的类型为Java基本类型。
  • sz::数组头元素的字节数
  • ebt:数组元素的类型,值取自枚举BasicType类中定义的枚举常量
  • esz:以字节为单位的数组元素大小
_name Java类型名称,如java/lang/String,[Ljava/lang/String;
_primary_supers

_primary_supers代表了这个类的父类,其类型是个Klass指针数组,默认的大小为8,可通过命令更改。例如IOException是Exception的子类,

而Exception又是Throwable的子类。所以表示IOException类的_primary_supers属性值为:

[Throwable, Exception, IOException]。如果继承链过长,也就是当前类加上继承的父类多于8个时,

会将多出来的类存储到secondary_supers数组中

 _super_check_offset 

快速查找supertype的一个偏移量,这个偏移量是相对于Klass实例起始地址的偏移量。如果当前表示的类是IOException,

那么这个属性就指向_primary_supers数组中存储IOException的位置。当存储的类多于8个时,值与secondary_super_cache

相等

_secondary_supers

Klass指针数组,一般存储Java类实现的接口,偶尔还会存储Java类及其父类

_secondary_super_cache

Klass指针,保存上一次查询父类的结果

_java_mirror oopDesc类型的指针,保存的是当前Klass对象表示的Java类所对应的java.lang.Class对象,可以据此访问类的静态属性
_super Klass指针,指向Java类的直接父类 
_subklass Klass指针,指向Java类的直接子类,由于直接子类可能有多个,所以通过_next_sibling连接起来
_next_sibling Klass指针,该类的下一个子类,通过调用_subklass->next_sibling()获取_subklass的兄弟子类
_next_link Klass指针,所有的由同一个类加载器加载的Java类都通过_next_link连接成一个单链表
_class_loader_data ClassLoaderData指针,可以通过此属性找到加载该Java类的ClassLoader
_access_flags 获取Java类型的修饰符,如private、final、static、abstract 、native等。可区分出当前的Java类型是接口还是类。
_prototype_header 在偏向锁的实现过程中非常重要,后续在介绍锁时会介绍 

能够通过Klass类中的相关属性保存Java类型定义的一些信息,如_name保存Java类的名称、_super保存Java类型的父类等。Klass类是Klass模型中定义的C++类的基类,所以此类对象只保存了Java类型的一些必要信息,其它如常量池、方法、字段等会通过Klass类的具体子类的相关属性来保存。 

类的属性比较多,我们在后面解析类的过程中可以看到对相关属性的赋值操作。

1、_layout_helper

_layout_helper是一个组合属性。如果当前的类表示一个Java数组类型时,这个属性的值比较复杂。通常会调用如下函数生成值:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.cpp

jint Klass::array_layout_helper(BasicType etype) {

  bool  isobj = (etype == T_OBJECT);
  int   tag   =  isobj ? _lh_array_tag_obj_value : _lh_array_tag_type_value;
  // 在64位系统下,存放_metadata的空间大小是8字节,_mark是8字节,
  // length是4字节,对象头为20字节,由于要按8字节对齐,所以会填充4字节,最终占用24字节
  int   hsize = arrayOopDesc::base_offset_in_bytes(etype); // hsize表示数组头部大小
  // Java元素类型需要占用的字节数
  int   esize = type2aelembytes(etype);
  // 例如Java基本类型元素占用4个字节,则存储的是2
  int   esz = exact_log2(esize);
  int   lh = array_layout_helper(tag, hsize, etype, esz);

  return lh;
}

表示Java数组类型的_layout_helper属性由4部分组成,下面分别介绍。

1tag

如果数组元素的类型为对象类型,值为0x80;否则值为0xC0,表示数组元素的类型为Java基本类型其中用到2个枚举常量,如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.hpp

_lh_array_tag_type_value    = ~0x00, 
_lh_array_tag_obj_value     = ~0x01

_lh_array_tag_type_value的二进制表示为32个1:11111111111111111111111111111111,其实也就是0xC0000000 >> 30,做算术右移,负数的最高位补1。

_lh_array_tag_obj_value的二进制表示为最高位31个1:11111111111111111111111111111110,其实也就是0x80000000 >> 30,做算术右移,负数的最高位补1。

2hsize

hsize表示数组头元素的字节数。调用arrayOopDesc::base_offset_in_bytes()及相关函数获取,实现如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/arrayOop.hpp

static int base_offset_in_bytes(BasicType type) {
    return header_size(type) * HeapWordSize;
}

static int header_size(BasicType type) {
    size_t typesize_in_bytes = header_size_in_bytes();
    return (int)(Universe::element_type_should_be_aligned(type)
         ? align_object_offset(typesize_in_bytes/HeapWordSize)
         : typesize_in_bytes/HeapWordSize);
}

// 在64位系统默认参数下,_metadata占用的内存大小是8字节,_mark是8字节,
// length是4字节,对象头为20字节,由于要按8字节对齐,所以会填充4字节,
// 最终占用24字节
static int header_size_in_bytes() {
    size_t hs = align_size_up( length_offset_in_bytes() + sizeof(int) , HeapWordSize );
    return (int)hs;
}

static int length_offset_in_bytes() {
    return UseCompressedClassPointers ? klass_gap_offset_in_bytes() : sizeof(arrayOopDesc);
}

length_offset_in_bytes()函数中,使用-XX:+UseCompressedClassPointers选项来压缩类指针,默认的值为truesizeof(arrayOopDesc)返回的值为16,其中_mark_metadata._klass各点用8字节。在压缩指针的情况下,_mark占用8字节,_metadata._narrowKlass占用4字节,共12字节。

3etypeesize

etype表示数组元素的类型,esize表示数组元素的大小。

最终会在Klass::array_layout_helper()函数中调用array_layout_helper()函数完成属性值的计算。这个函数的实现如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.hpp

static jint array_layout_helper(jint tag, int hsize, BasicType etype, int log2_esize) {
    return (tag        << _lh_array_tag_shift)  // 左移30位
      |    (hsize      << _lh_header_size_shift)  // 左移16位
      |    ((int)etype << _lh_element_type_shift)  // 左移8位
      |    (log2_esize << _lh_log2_element_size_shift); // 左移0位
}

最终计算出来的数组类型的_layout_helper值为负数,因为最高位为1,而对象类型通常是一个正数,这样就可以简单的通过判断_layout_helper值来区分数组和对象。_layout_helper最终的布局如下图所示。

对_lh_array_tag_type_value与_lh_array_tag_obj_value值左移30位后,第32位上肯定为1,所以最终计算出的值是一个小于0的数。而非数组类型,一般由InstanceKlass对象表示的Java类来说,计算的属性值如下:

 static jint instance_layout_helper(jint size, bool slow_path_flag) {
	  if(slow_path_flag){
		  return  (size << LogHeapWordSize)  | _lh_instance_slow_path_bit;
	  }else{
		  return  (size << LogHeapWordSize)  | 0; // LogHeapWordSize=3
	  }
  }

size为对象的、以字节为单位的内存占用大小,所以肯定是一个正数。这样就可以通过_layout_helper来判断类型了。 

  

2、_primary_supers、_super_check_offset、_secondary_supers与_secondary_super_cache

这几个属性完全是为了加快判定父子关系等逻辑而加入的。下面看initialize_supers()函数中是如何初始化这几个属性的。函数的第1部分实现如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.cpp 

void Klass::initialize_supers(Klass* k, TRAPS) {
  // 当前类的父类k可能为NULL,例如Object的父类为NULL
  if (k == NULL) {
    set_super(NULL);
    _primary_supers[0] = this;
  } 
  // k就是当前类型的直接父类,如果有父类,那么super()获取的Klass::_super属性的值通常为NULL,因为还没有设置此属性的值。
else if (k != super() || k == SystemDictionary::Object_klass()) { assert(super() == NULL || super() == SystemDictionary::Object_klass(), "initialize this only once to a non-trivial value"); set_super(k); // 通过Klass::_super属性保存当前类型的父类 Klass* sup = k;
// 当sup存储在_secondary_supers数组中时,super_depth()返回默认的值8,否则返回在_primary_supers数组中存储的位置 int sup_depth = sup->super_depth(); // 调用primary_super_limit()函数得到的默认值为8 juint my_depth = MIN2(sup_depth + 1, (int)primary_super_limit()); // 当父类的继承链长度大于等于primary_super_limit()时,当前的深度只能是primary_super_limit(), // 也就是8,因为_primary_supers数组中最多只能保存8个类 if (!can_be_primary_super_slow()) my_depth = primary_super_limit(); // my_depth默认的值为8 // 将直接父类的继承类拷贝到_primary_supers中,因为直接父类和当前子类肯定有共同的继承链 for (juint i = 0; i < my_depth; i++) { _primary_supers[i] = sup->_primary_supers[i]; } Klass* *super_check_cell; if (my_depth < primary_super_limit()) { _primary_supers[my_depth] = this; // 将当前类也存储在_primary_supers中 super_check_cell = &_primary_supers[my_depth]; } else { 需要将部分父类放入_secondary_supers数组中 super_check_cell = &_secondary_super_cache; } // 设置Klass::_super_check_offset属性的值 set_super_check_offset((address)super_check_cell - (address) this); // 省略了第2部分代码 }

在设置当前类型的父类时通常都会调用initialize_supers()函数,同时也会设置_primary_supers_super_check_offset属性的值,如果继承链过长,还有可能设置_secondary_supers_secondary_super_cache属性的值。这些属性中保存的信息可以快速的进行类关系之间的判断,例如父子关系的判断。

函数的第2部分代码实现如下:

if (secondary_supers() == NULL) {
    KlassHandle this_kh (THREAD, this);

    int extras = 0;
    Klass* p;
    // 当p不为NULL并且p已经存储在了_secondary_supers数组中时,条件为true
    // 也就是当前类的父类多于8个,需要将多出来的存储到_secondary_supers数组中
    for (p = super(); !(p == NULL || p->can_be_primary_super()); p = p->super()) {
      ++extras;
    }

    // 计算secondaries需要的大小,因为secondaries数组中还需要存储当前类型的
    // 所有实现接口(包括直接和间接实现的接口)
    // 如果当前是接口时,那么计算出的extras为0,那么如下函数就会直接设置_secondary_supers为
    // 所有的接口,然后返回NULL,所以接口通常会在这里返回
    GrowableArray<Klass*>* secondaries = compute_secondary_supers(extras);
    if (secondaries == NULL) {
      return;
    }

    // 将无法存储在_primary_supers中的类暂时存储在primaries中
    GrowableArray<Klass*>* primaries = new GrowableArray<Klass*>(extras);
    for (p = this_kh->super(); !(p == NULL || p->can_be_primary_super()); p = p->super()) {
      ...
      primaries->push(p);
    }
    
    int new_length = primaries->length() + secondaries->length();
    Array<Klass*>* s2 = MetadataFactory::new_array<Klass*>(class_loader_data(), new_length, CHECK);
    int fill_p = primaries->length();
    for (int j = 0; j < fill_p; j++) {
      // 这样的设置会让父类永远在s2数组的前面,子类在父类之后存储
      s2->at_put(j, primaries->pop());  
    }
    for( int j = 0; j < secondaries->length(); j++ ) {
      // 类存储在s2数组前面,接口存储在数组的后面
      s2->at_put(j+fill_p, secondaries->at(j));  
    }

    this_kh->set_secondary_supers(s2); // 设置_secondary_supers属性的值
  }

可以看到,会将当前类型继承链中多于8个的父类存储到_secondary_supers数组中,不过因为继承链一般都不会多于8个,所以设置了默认值为8,避免过大的数组浪费太多的内存。

下面举个例子,看看这几个属性是如何存储值的,如下:

interface IA{}
interface IB{}

class A{}
class B extends A{}
class C extends B{}
class D extends C{}
public class Test extends D implements IA,IB {}

配置-XX:FastSuperclassLimit=3后,_primary_supers数组中就最多只能存储3个类了。值如下:

_primary_supers[Object,A,B]
_secondary_supers[C,D,IA,IB]

由于当前类Test的继承链过长,导致C和D只能存储到_secondary_supers。所以此时_super_check_offset会指向C,也就是_secondary_supers中存储的第1个元素。

再举个例子,如下:

interface IA{}
interface IB extends IA{}

public interface Test extends IB{}

假设当前的Test为接口,那么值如下:

_primary_supers[Object]
_secondary_supers[IA,IB]

下面举个例子,看一下这几个属性如何应用。例如is_subtype_of()函数,实现如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.hpp

// 判断当前类是否为k的子类。k可能为接口,如果当前类型实现了k接口,函数也返回true
bool is_subtype_of(Klass* k) const {
    juint off = k->super_check_offset();
    Klass* sup = *(Klass**)( (address)this + off );
    const juint secondary_offset = in_bytes(secondary_super_cache_offset());
    // 如果k在_primary_supers中,那么利用_primary_supers一定能判断出
    // k与当前类的父子关系
    if (sup == k) {
      return true;
    }
    // 如果k存储在_secondary_supers中,
    // 如果两者有父子关系,那么_super_check_offset需要与_secondary_super_cache相等
    else if (off != secondary_offset) {
      return false;
    }
    // 可能有父子关系,需要进一步判断
    else {
      return search_secondary_supers(k);
    }
}

当通过_super_check_offset获取到的类与k相同时,那么k存在于当前类的继承链上,肯定有父子关系。

如果k存在于_primary_supers数组中,那么通过_super_check_offset就可快速判断,如果k存在于_secondary_supers中,那么需要调用search_secondary_supers()来判断。

调用的search_secondary_supers()函数的实现如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.cpp

bool Klass::search_secondary_supers(Klass* k) const {
  if (this == k)
    return true;

  // 通过_secondary_supers中存储的信息进行判断
  int cnt = secondary_supers()->length();
  for (int i = 0; i < cnt; i++) {
    if (secondary_supers()->at(i) == k) {
      ((Klass*)this)->set_secondary_super_cache(k);
      return true;
    }
  }
  return false;
}

可以看到,属性_secondary_super_cache保存了这一次父类查询的结果。查询的逻辑很简单,遍历_secondary_supers数组中的值并比较即可。 

3、_super、_subklass、_next_sibling 

由于Java类是单继承,所以可通过_super、_subklass、_next_sibling属性可直接找到当前类型的父类型或所有子类型。调用Klass::append_to_sibling_list()函数设置_next_sibling与_subklass属性的值,函数的实现如下:

源代码位置:openjdk/hotspot/src/share/vm/oops/klass.cpp

void Klass::append_to_sibling_list() {
  InstanceKlass* super = superklass();   // 获取到_super属性的值
  if (super == NULL) 
    return;   // 如果Klass实例表示的是Object类时,此类没有超类

  // super可能有多个子类,多个子类会用_next_sibling属性连接成单链表,当前的类是链表头元素
  Klass* prev_first_subklass = super->subklass_oop();  // 获取_subklass属性的值
  if (prev_first_subklass != NULL) {
    set_next_sibling(prev_first_subklass);  // 设置_next_sibling属性的值
  }
  super->set_subklass(this);  // 设置_subklass属性的值
}

函数的实现逻辑很简单,这里不过多介绍。  

 

posted on 2020-11-20 06:44  鸠摩(马智)  阅读(2020)  评论(2编辑  收藏  举报

导航