专注虚拟机与编译器研究

第2.6篇-HotSpot VM的对象模型(6)

接着上一篇,我们继续来讲oopDesc相关的子类。

3、instanceOopDesc类

instanceOopDesc类的实例表示除数组对象外的其它对象。在HotSpot VM中,对象在内存中存储的布局可以分为三块区域:对象头(header)、对象字段数据(field data)和对齐填充(padding),如下图所示。 

 

下面详细介绍一下这3个组成部分。

1.对象头

可以看到对象头分为两个部分,一个就是“Mark Word”,另外还有存储指向元数据区对象类型数据的指针_klass或_compressed_klass。这两个都在介绍oopDesc类时详细介绍过,这里不再介绍。

2对象字段数据

Java对象中的字段数据存储Java源代码中定义的各种类型字段内容,具体包括父类继承及子类定义的字段。

存储顺序受到HotSpot VM布局策略的命令-XX:FieldsAllocationStyle和字段在Java源代码中定义顺序的影响。默认布局策略的顺序为:long/doubleintshort/charbooleanoop(对象指针,32位系统占用4字节,64位系统占用8字节),相同宽度的字段总被分配到一起。

如果虚拟机的-XX:+CompactFields参数为true,子类中较窄的变量可能插入到空隙中,以节省使用的内存空间。例如,当首先布局long/double类型的字段时,由于对齐的原因,可能会在headerlong/double字段之间形成空隙,例如64位系统开启压缩指针,header12个字节,而long/double类型的字段需要从第16个字节处开始存储,这样就形成了4个字节就是空隙。这时就可以将一些短类型插入long/doubleheader之间的空隙中。

3对齐填充部分

对齐填充部分不是必须的,只起占位符作用,没有其他含义。HotSpot VM要求对象大小必须是8字节的整数倍,对象头是8字节整数倍,所以填充是对实例数据没有对齐的情况来说的。对象大小如果为8字节对齐,那么对象在内存中线性分配时,对象头的地址就是8字节对齐的,这时候就为对象指针压缩提供了条件,可以将地址缩小8倍后存储在32位的地址中。

在创建instanceOop实例时会调用allocate_instance()函数,这个函数的实现如下:

 

源代码位置:openjdk/hotspot/src/share/vm/oops/instanceKlass.cpp
 
instanceOop InstanceKlass::allocate_instance(TRAPS) {
  int size = size_helper();  

  KlassHandle h_k(THREAD, this);

  instanceOop i;
  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  ...
  return i;
}

调用instanceKlass类中的size_helper()函数获取创建instanceOop实例所需要的内存大小,调用CollectedHeap::obj_allocate()函数分配size大小的内存。size_helper()函数在之前介绍过,就是从_layout_helper属性中获取Java对象所需要的内存大小。如下:

int size_helper() const {
    return layout_helper_to_size_helper(layout_helper());
}

int layout_helper() const  { 
   return _layout_helper; 
}

static int layout_helper_to_size_helper(jint lh) {
    assert(lh > (jint)_lh_neutral_value, "must be instance");
    // Note that the following expression discards _lh_instance_slow_path_bit.
    return lh >> LogHeapWordSize; 
}

从_layout_helper属性中获取大小,之前介绍过这个综合描述符,如果为InstanceKlass,则组合数字中含有的是instanceOop对象的大小,在设置时调用的是instance_layout_helper()方法,如下:

static jint instance_layout_helper(jint size, bool slow_path_flag) {
    return (size << LogHeapWordSize)  // LogHeapWordSize=3
      |    (slow_path_flag ? _lh_instance_slow_path_bit : 0); // 实例慢速分配有关
}

获取size时需要向右移动3位即可。这个方法在创建InstanceKlass对象时会调用,不过size通常会初始化为0,在调用parseClassFile()方法计算完实例的大小时,还会调用此方法更新为真正需要的instanceOop对象大小,在解析类文件时会详细介绍实例大小的计算过程。 

在InstanceKlass::allocate_instance()函数中调用CollectedHeap::obj_allocate()函数分配size大小的内存并将内存初始化为零值,这样Java对象就可以不经过初始化而使用其各个字段的零值。 

4、arrayOopDesc类

arrayOopDesc类的实例表示Java数组对象。具体的基本类型数组或对象类型数组由具体的C++中定义的子类实例表示。在HotSpot VM中,数组对象在内存中的布局可以分为三块区域:对象头(header)、对象字段数据(field data)和对齐填充(padding),如下图所示。

 

Java对象内存布局唯一不同之处在于,数组对象的对象头中还会存储数组的长度length,占用的内存空间为4字节。在64位系统下,存放_metadata的空间大小是8字节,_mark8字节,length4字节,对象头为20字节,由于要按8字节对齐,所以会填充4字节,最终占用24字节。64位开启指针压缩的情况下,存放_metadata的空间大小是4字节,_mark8字节,length4字节,对象头为16字节。

5、arrayOopDesc类的子类 

arrayOopDesc类的子类有2个,分别为表示组件类型为基本类型的typeArrayOopDesc和表示组件类型为对象类型的objArrayOopDesc。二维及二维以上的数组都用objArrayOopDesc的实例来表示。如typeArrayOopDesc表示组件类型为基本类型的一维数组,如果是二维或多维数组,那么组件类型就是数组,而数组是对象类型,所以会用objArrayOopDesc实例来表示。

当需要创建typeArrayOopDesc实例时,通常会调用oopFactory类中定义的工厂方法,例如调用new_boolArray()函数创建一个boolean数组,如下: 

源代码位置:openjdk/hotspot/src/share/vm/memory/oopFactory.hpp

static typeArrayOop  new_boolArray (int length, TRAPS) {
  return TypeArrayKlass::cast(Universe::boolArrayKlassObj())->allocate(length, CHECK_NULL); 
} 

调用Universe::boolArrayKlassObj()函数获取_boolArrayKlassObj属性的值,此值是通过调用TypeArrayKlass::create_klass()函数创建的、表示boolean数组的TypeArrayKlass实例,在2.1.5节介绍过。然后调用TypeArrayKlass类中的allocate()函数创建typeArrayOop实例,如下:

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

typeArrayOop allocate(int length, TRAPS) {
	  return allocate_common(length, true, THREAD);
}

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

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

typeArrayOop TypeArrayKlass::allocate_common(int length, bool do_zero, TRAPS) {
  assert(log2_element_size() >= 0, "bad scale");
  if (length >= 0) {
    if (length <= max_length()) {
      size_t size = typeArrayOopDesc::object_size(layout_helper(), length);
      KlassHandle h_k(THREAD, this);
      typeArrayOop t;
      CollectedHeap* ch = Universe::heap();
      if (do_zero) {
        t = (typeArrayOop)CollectedHeap::array_allocate(h_k, (int)size, length, CHECK_NULL);
      } else {
        t = (typeArrayOop)CollectedHeap::array_allocate_nozero(h_k, (int)size, length, CHECK_NULL);
      }
      return t;
    } else {
      // 抛出异常
    }
  } else {
    // 抛出异常
  }
}

参数length表示创建数组的大小,而do_zero表示是否需要在分配数组内存时,将内存初始化为零值。函数首先调用typeArrayOopDesc::object_size()函数从_layout_helper中获取数组的大小,然后就是调用array_allocate()array_allocate_nozero()函数分配内存并初始化对象头,具体就是为lenth_mark_metadata属性赋值。

typeArrayOopDesc::object_size()函数的实现如下:

 

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

static int object_size(int lh, int length) {
    int instance_header_size = Klass::layout_helper_header_size(lh);
    int element_shift = Klass::layout_helper_log2_element_size(lh);

    // 长度为length的数组需要占用的内存大小
    julong size_in_bytes = length;
    size_in_bytes <<= element_shift;

    // 加上对象头的大小后,进行内存对齐即可
    size_in_bytes += instance_header_size;
    julong size_in_words = ((size_in_bytes + (HeapWordSize-1)) >> LogHeapWordSize);

    return align_object_size((intptr_t)size_in_words);
}

 

2.2.2节介绍过,ArrayKlass实例的_layout_helper属性是个组合数字,可以通过调用对应的函数从这个属性中获取到数组头需要占用的字节数以及组件类型需要占用的字节数,如果组件类型为boolean类型,这个值为1。最终arrayOopDesc实例的大小是通过如下公式计算出来的:

size = instance_header_size + length<<element_shift + 对齐填充

也就是对象头加上实例数据,然后再加上对齐填充。

TypeArrayKlass::allocate_common()函数中获取到TypeArrayOopDesc实例所需要分配的内存大小后,就会调用CollectedHeap::array_allocate()CollectedHeap::array_allocate_nozero()函数在堆上分配内存空间,然后初始化对象头信息。2个函数的实现类似,我们只看CollectedHeap::array_allocate()函数的实现即可,如下:

oop CollectedHeap::array_allocate(
KlassHandle klass,
int size,
int length,
TRAPS
) {
  HeapWord* obj = common_mem_allocate_init(klass, size, CHECK_NULL);
  post_allocation_setup_array(klass, obj, length);
  return (oop)obj;
}

调用common_mem_allocate_init()函数在堆上分配指定size大小的内存,关于在堆上分配内存的知识点将在第9章详细介绍,这里不做介绍。调用post_allocation_setup_array()函数初始化对象头,如下:

 

void CollectedHeap::post_allocation_setup_array(
 KlassHandle klass,
 HeapWord* obj,
 int length
) {
  // 初始化数组中的length属性
  ((arrayOop)obj)->set_length(length);
  post_allocation_setup_common(klass, obj);
}

void CollectedHeap::post_allocation_setup_common(
 KlassHandle klass,
 HeapWord* obj
) {
  post_allocation_setup_no_klass_install(klass, obj);
  post_allocation_install_obj_klass(klass, oop(obj));
}

void CollectedHeap::post_allocation_setup_no_klass_install(
 KlassHandle klass,
 HeapWord* objPtr
) {
  oop obj = (oop)objPtr;
  // 在允许使用偏向锁的情况下,获取Klass中的_prototype_header属性值,其中的锁状态
  // 一般为偏向锁状态,而markOopDesc::prototype()函数初始化的对象头,锁状态一般为正常
  // Klass中的_prototype_header完全是为了支持偏向锁增加的属性,后面将会详细介绍
  // 偏向锁的实现机制
  if (UseBiasedLocking && (klass() != NULL)) {
    obj->set_mark(klass->prototype_header());
  } else {
    obj->set_mark(markOopDesc::prototype());
  }
}

void CollectedHeap::post_allocation_install_obj_klass(KlassHandle klass,oop obj) {
  obj->set_klass(klass());
}

 

调用的函数比较多,但是实现非常简单,这里不做过多介绍。

objArrayOop的创建与typeArrayOop的创建非常类似,也是调用oopFactory类中的工厂方法new_objectArray()函数,然后调用ObjArrayKlass::allocate()函数分配内存,这里不在介绍。

 

posted on 2020-07-13 08:36  鸠摩(马智)  阅读(1212)  评论(0编辑  收藏  举报

导航