Android 对象内存布局

1,744 阅读10分钟

JVM 上有 JOL 可以查看对象的内存布局,而 ART 并非 JVM 的实现,所以 JOL 并不适用于 ART。

而本文将在《Android 对象内存占用》的基础上,讨论 ART 上对象的内存布局。更进一步,会以本文中提及的知识点做一个工具以展示 ART 平台上对象的内存布局。

概述

内存布局指对象的内部属性在内存中的布局信息,即对象其字段在内存中的排列方式。在目前的 ART 代码中(android11-release),对象在内存中的布局可分为两个区域:对象头(Object Header)、实例数据(Instance Data),其中实例数据区域可能包含对齐填充(Padding or Gap,下文会解析为什么用两种说法)。

接下来,本文会介绍 Object Header、Instance Data 的排列方式。此外,还会讲述如何在应用层获取对象的布局信息。在开始前,先了解一些基本概念:

  • 字段偏移(Field Offset):每个 Field 都会有自己的起始位,加上 Field 其对应的类型所占用的字节数(byte size),可以直接读出 Field 对应的值
  • 对齐填充(Padding or Gap):不是必然存在,ART 对 Fields 进行布局的过程中会产生 gap,而分配空间的过程中,可能会产生 padding(视 Allocator 或对象类型而定)
  • 实例字段(Instace Fields):非静态字段,实例对象拥有的字段

另外,本文中的内容只讨论 ART 默认配置的行为,有兴趣了解其他配置下行为差异的,可自行阅读源码。

布局方式

对象头(Object Header)

对象头即为 Object 对象自身包含的 fields 的占用空间

// art/runtime/mirror/object.h

// C++ mirror of java.lang.Object
class MANAGED LOCKABLE Object {
 private:
  // The Class representing the type of the object.
  HeapReference<Class> klass_;
  // Monitor and hash code information.
  uint32_t monitor_;
}

ART 中对象头包含的数据与 HotSpot 中差不多,其中 klass_类型指针,指向对应的 Class。而 monitor_ 对应的是 LockWord,类似于 HotSpot 的 MarkWord

代码中 HeapReference 类型实际是 uint32_t,所以对象头的大小为 8 bytes。

而实例数据中,第一个 field 的 offset 即为对象头的大小。

实例数据(Instance Data)

实例数据即为 instace fields。按照 ART 代码中的命名,在类链接(Linking)阶段中,链接字段(Link Fields)时所产生的 padding 称为 gap。本文为了区别于对象的对齐,用 gap 指代 fields 对齐所产生的对齐填充,padding 则指分配对象空间等情况下发生的对齐填充。

Padding

在 ART 中,默认的 allocator 下只有 String 分配空间时会进行对齐填充(8 bytes)。非默认配置时,若 allocator 为 kAllocatorTypeBumpPointerkAllocatorTypeTLABkAllocatorTypeRegionkAllocatorTypeRegionTLAB,分配对象空间均会以 8 bytes 对齐。由于本文只讨论默认配置的逻辑,不再深入讨论 padding。

Gap

Link Fields 阶段所有 instance fields 会先按 object reference -> long -> double -> int -> float -> char -> short -> boolean -> byte 的顺序进行排序,再按序填入 fields。每填入一个 field 会判断当前的 offset 是否为 field 对应类型所占用字节数的倍数。若不是对应的倍数,会对 field 进行对齐使 offset 变为类型大小的倍数。另外,为了保证不浪费太多空间,当后续的 field 占用大小满足小于或等于 gap 时,field 会填入 gap 中。

关于 gap 的内容可能有点抽象,下面以两个例子展开。

未利用 gap

首先是会产生 gap 但没有利用 gap 的情况

class Foo {
    private val obj: Any? = null
    private val aLong: Long = 3L
}

Foo 按照上述规则处理后,最终存在 4 bytes 的 gap,具体过程如下:

  1. Fields 排序,结果为 obj -> aLong
  2. 将对象头大小作为起始 offset,默认情况下为 8
  3. 按照顺序, obj 为引用类型,其占用大小为 4(后面会解释),8 为 4 的倍数,不需要添加 gap
  4. 填入 obj,其 offset 为 8
  5. 更新当前 offset,offset += 4,即 12
  6. aLong 为 long,其占用大小为 8,而当前 offset 为 12,12 不是 8 的倍数,添加 4 bytes 的空间
  7. 根据 gap 更新 offset,当前 offset 为 16
  8. 填入 aLong,其 offset 为 16

最终 Foo 的布局如下:

com.chaos.aol.sample.Foo object internals:
 OFFSET  SIZE               TYPE  DESCRIPTION                               VALUE
      0     4                     (object header)                           13 07 cc c8 (00010011 00000111 11001100 11001000) (319278280)
      4     4                     (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4   java.lang.Object  Foo.obj                                   null
     12     4                     (alignment/padding gap)
     16     8               long  Foo.aLong                                 3
Instance size: 24 bytes (JVMTI: 24 bytes)
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
利用 gap

再来说下产生了 gap 且 gap 被利用的情况

class Bar {
    private val obj: Any? = null
    private val aInt: Int = 1
    private val aLong: Long = 3L
}

Bar 按照上述规则处理后,最终不存在 gap,具体过程如下:

  1. Fields 排序,结果为 obj -> aLong -> aInt
  2. (与 Foo 流程一致,此处省略)
  3. 由于 int 大小为 4,与 aLong 对齐过程中产生的 4 bytes gap 相等,满足填入 gap 的条件,直接填入 gap

最终 Bar 的布局如下:

com.chaos.aol.sample.Bar object internals:
 OFFSET  SIZE               TYPE  DESCRIPTION                               VALUE
      0     4                     (object header)                           13 32 e9 88 (00010011 00110010 11101001 10001000) (322103688)
      4     4                     (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4   java.lang.Object  Bar.obj                                   null
     12     4                int  Bar.aInt                                  1
     16     8               long  Bar.aLong                                 3
Instance size: 24 bytes (JVMTI: 24 bytes)
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可见相对 Foo,[12, 16) 这个范围内的数据被填入了 aInt

注意:若填入的数据比 gap 小,gap 会更新等待下一次利用。比如某对象中有 Object, long, char, boolean 四个 fields,填入 char 后 gap 被更新为 2,再填入 boolean 会更新为 1

获取布局信息

从上述内容中可以知道,对于整体布局信息,我们需要知道对象头大小、各 field 的 offset、field 占用大小、gap 的大小。另外,还需要将对象总大小与最后一个 field 的结束位进行对比以得知对象是否存在 padding。

即要获取布局信息,我们需要计算出如下信息:

  1. 对象头大小
  2. 对象中每个 field 的 offset
  3. field 对应类型所占 bytes
  4. field 对齐时产生的 gap
  5. 对象总大小

对象头大小

因为 native 的 ::art::mirror:Object 对应 java.lang.Object,我们可以反射读取 java.lang.Object.classobjectSize 获取。

Field Offset

Field 的排序、对齐等操作对我们来说比较麻烦。而且 native 层的 fields 即使在 java 层有对应的描述,也不一定能通过反射获取,无法自行计算。所幸即使我们无法读取对象实际存在的所有 fields,但也可以通过 UnsafeobjectFieldOffset 方法获取 Field 准确的 offset。

Field 所占大小

这部分直接看 ART 的源码

// art/runtime/art_field-inl.h

inline size_t ArtField::FieldSize() REQUIRES_SHARED(Locks::mutator_lock_) {
  return Primitive::ComponentSize(GetTypeAsPrimitiveType());
}

// art/libdexfile/dex/primitive.h

static constexpr size_t kObjectReferenceSize = 4;

class Primitive {
  static constexpr size_t ComponentSize(Type type) {
    switch (type) {
      case kPrimVoid:    return 0;
      case kPrimBoolean:
      case kPrimByte:    return 1;
      case kPrimChar:
      case kPrimShort:   return 2;
      case kPrimInt:
      case kPrimFloat:   return 4;
      case kPrimLong:
      case kPrimDouble:  return 8;
      case kPrimNot:     return kObjectReferenceSize;
    }
    LOG(FATAL) << "Invalid type " << static_cast<int>(type);
    UNREACHABLE();
  }
}

从上述代码中,可以得出类型占用大小关系如下表:

类型字节数
void0
boolean1
byte1
char2
short2
int4
float4
long8
double8
object_reference4

产生的 Gap

由于 field 之间布局时有明确的顺序,我们可以通过如下公式求得两个 field 之间的 gap 大小:

// 假设内存布局中 second 为 first 的后一个 field
gap = second.offset - first.offset - first.占用大小

另外,还需要考虑最后的 field 后面的 gap 或 padding 的大小。我们可以用对象的总大小替代 second.offset,这样就能得出对象尾部的 gap 或 padding 的大小。

对象总大小

按照《Android 对象内存占用》一文,需要分四种情况进行计算:

  1. 数组对象
  2. String 对象
  3. Class 对象
  4. 普通对象

数组对象

我们需要知道数组对象 Header 大小及其数据大小,两者相加即数组对象的总大小。

在这介绍下 Unsafe 的两个方法:

  • int arrayBaseOffset(Class) —— 返回值为数组数据储存的起始位
  • int arrayIndexScale(Class) —— 返回值为数组单个元素占用的大小

其中 Array Header 等同于数据起始位,而数据大小为单个元素大小与数组长度的积,可以数组对象大小的计算可以通过如下公式求出:

size = arrayBaseOffset(clazz) + arrayIndexScale(clazz) * length

String 对象

根据《Android 对象内存占用》中的公式处理即可

size = RoundUp(16 + (IsCompressed() ? 1 : 2) * length,  8);

其中 IsCompressed() 在 String 的所有字符为 ASCII 时为 true,否则为 false。RoundUp(x, 8) 则是以 8 bytes 对齐。

Class 对象

Class 对象的 classSize 即为 Class 对象的大小,由于反射并不能拿到 classSize 对应的 Field,需另辟蹊径。

Unsafe 中有一个 getInt(Object obj, long offset) 方法,如果我们可以计算出 classSize 对应的 offset,我们就能 getInt 拿到 classSize 对应的值。

为了实现这点,先看下 ART 中 Class 的定义:

// art/runtime/mirror/class.h

// C++ mirror of java.lang.Class
class MANAGED Class final : public Object {
 private:
  HeapReference<ClassLoader> class_loader_;
  HeapReference<Class> component_type_;
  HeapReference<DexCache> dex_cache_;
  HeapReference<ClassExt> ext_data_;
  HeapReference<IfTable> iftable_;
  HeapReference<String> name_;
  HeapReference<Class> super_class_;
  HeapReference<PointerArray> vtable_;
  uint64_t ifields_;
  uint64_t methods_;
  uint64_t sfields_;
  uint32_t access_flags_;
  uint32_t class_flags_;
  uint32_t class_size_;
  pid_t clinit_thread_id_;// 实际为 int32_t
  int32_t dex_class_def_idx_;
  int32_t dex_type_idx_;
  uint32_t num_reference_instance_fields_;
  uint32_t num_reference_static_fields_;
  uint32_t object_size_;
  uint32_t object_size_alloc_fast_path_;
  uint32_t primitive_type_;
  uint32_t reference_instance_offsets_;
  uint32_t status_;
  uint16_t copied_methods_offset_;
  uint16_t virtual_methods_offset_;
};

可见所有 fields 的定义规则与 link fields 的排序规则一致,且 class_size_ 前有两个 uint32_t。这意味着即使前面的 fields 有出现对齐填充,class_size_ 都不可能被填入前面产生的 gap 中。

试想一下,假设 HeapReference 部分发生对齐填充,产生的 gap g1 必定小于 4。uint64_t 部分也发生填充,产生的 gap g2 必定小于 8。而 uint32_t 大小为 4,第一个 uint32_t(即 access_flags_)只能填入 g2,后面的 uint32_t 则会按照定义的顺序进行布局。所以 class_size_ 不可能被填入 g1g2 中。

那么,只要拿到 class_flags_ 或者 clinit_thread_id_ 的 offset,就能计算出 class_size_ 的 offset。而 clinit_thread_id_ 在 java 层对应的 clinitThreadId 是可以读取到的,最终可以通过如下方法获取 classSize 对应的值:

val clinitThreadIdField =
        Class::class.java.getDeclaredField("clinitThreadId").apply { isAccessible = true }
val classSizeFieldOffset = Unsafe.objectFieldOffset(clinitThreadIdField) - 4/* int32_t */

fun sizeOfClassObject(clazz: Class<*>): Int = Unsafe.getInt(clazz, classSizeFieldOffset)

普通对象

对象对应的 Class 中的 objectSize 即为对象的大小,反射可以直接获取。

总结

最后总结一下,目前 ART 平台上对象布局主要分两区域:对象头、实例数据,其中实例数据区域可能存在 gap。另外,对于 String 对象或者特殊的 allocator,对象尾部还会存在 padding 区域。

而要获取对象的内存布局,关键的数据在对象头大小、field 的 offset、field 的大小、对象总大小(题外话,可以通过 JVMTI 的 GetObjectSize 获取对象总大小,该 API 最终调用的是 Object::SizeOf,能获取各类型对象准确大小)。

最后,其实了解对象的内存布局并不能给我们带来太多收益,只能让我们知道对象实际的大小与其定义并不一定相等。

笔者研究这个知识点主要是想做一个用于 ART 平台,类似于 JOL 的工具。目前已经完成初版,命名为 AOL(Android Object Layout)。(上面示例代码中 Foo, Bar 的内存布局信息就是通过 AOL 直接获取)

参考内容