【Android】《深入理解 Android 虚拟机 ART》 读书笔记(一)——Space 家族概述


ART 中与内存分配相关的类的继承关系

  • 第一层:AllocSpace 和 Space,Space 代表了一块内存空间,AllocSpace 代表了可用于内存分配的空间,提供了内存分配、释放相关的虚函数
  • 第二层:ContinuousSpace 和 DiscontinuousSpace 分别表示连续和非连续的内存空间
  • 第三层:MemMapSpace 表示内存空间中内存由内存映射技术提供,LargeObjectSpace 中的内存可以分配给外部使用,如果一个 Java 对象(必须为 String 或其他基本数据类型)所需空间超过三个内存页,,则使用 LargeObjectSpace 来提供内存资源
  • 第四层:ImageSpace 和 ContinuousMemMapAllocSpace 中, ImageSpace 用于 .art 文件的加载,一个 ImageSpace 创建成功,其对应的 .art 文件所包含的 mirroObject 对象就被加载到内存中。而 ContinuousMemMapAllocSpace 则代表了一个可以对外提供连续内存的空间,其内存资源由内存映射技术提供
  • 第五层:其中除 MallocSpace 外的其他类可以直接用于分配内存,其内存分配算法各有不同。
  • 第六层:它们同样用于内存分配,使用了不同的算法

ZygoteSpace

实际上不能分配内存及释放内存,它的创建需要外部传入 MemMap 的指针。

MirroObject: Java 中的 Java Object 在 ART 中就对应了一个 mirror Object,且 mirro Object 在 ART 中以 8 字节对齐。

BumpPointerSpace

BumpPointerSpace 提供了一种非常简单的内存分配算法——顺序分配,这种算法只需要一个变量记住上一次分配到的位置。但它存在一个问题——它无法释放某一次所分配的内存,只支持一次性释放所有的内存(实现了 AllocSpace 的 clear 函数)。

这种非常简单的内存分配、释放算法**适合用于线程的本地内存分配 **(Thread Local Allocation Blocks,简写为 TLAB),它代表了一块专属于某个线程的内存资源。

Alloc 与 AllocNewTlab

BumpPointerSpace 提供了两种内存分配的方式:

  • Alloc:用于为某个 mirror Object 对象分配所需内存
  • AllocNewTlab:当 ART 决定从『调用线程的本地存储空间』中分配内存时会调用此函数

Alloc 实现简述

Alloc 中调用了 AllocNonVirtual 函数,并设置了输出参数后返回。

而 AllocNonVirtual 函数又调用了 AllocNonvirtualWithoutAccounting 函数,并且对整个空间的 objects_allocated_ 及 bytes_allocated_ 进行了统计。这两个变量均是用 AtomicInteger 实现的,保证了多线程下的原子操作。

在 AllocNonvirtualWithoutAccounting 函数中,通过 ContinuousSpace 中的成员变量 end_ 获取到了上一次分配的末尾位置(end_ 为 Atomic<uint8_t*>,由于分配算法简单,只需使用这种原子变量即可实现并发操作),之后在内存足够的情况下分配内存并返回,若超过内存资源大小则会直接返回 nullptr。(end_ 会指向最新的末尾位置new_end,而 old_end 则指向了这次分配所得内存的起始位置(上一次的末尾位置)。

Alloc 在 AllocSpace 中的参数含义

Alloc 函数有五个参数:

  • self:调用线程的线程对象,由于 BumpPointerSpace 没有用到这个参数,因此其参数列表没有写出其参数名。
  • num_bytes:此次内存分配所需内存大小
  • bytes_allocated:实际分配的内存大小,是一个输出参数。若分配成功,它有可能大于或等于 num_bytes(有的内存分配算法实际会多分配一些内存存储所需特殊信息)
  • usable_size:分配的内存中可被外界使用的大小,是输出参数。若分配成功它会大于或等于 num_bytes。
  • bytes_tl_bulk_allocated:和 thread local 内存分配有关

而 Alloc 的返回值为一个 mirror::Object*,因此 Alloc 的作用是为 JavaObject(ART 中的 mirroObject)分配内存。

AllocNewTlab 实现简述

AllocNewTlab 函数主要是为传入的线程 self 分配指定大小的新 TLAB。它首先调用了 RevokeThreadLocalBuffersLocked 函数释放线程原来的 TLAB(其实 TLAB 就是一块内存),之后调用了 AllocBlock 函数分配内存并返回了起始位置 start。最后通过 self->SetTlab 函数设置了 TLAB 的起始与结束位置,通过 setTlab 函数会对 tlsPtr_ 的成员变量进行赋值。

TlsPtr

tlsPtr_ 是 Thread 类中定义的一个结构体,它内部有下面几个成员变量,具体解释见注释:

AllocBlock

在 AllocBlock 方法中,如果是第一次分配内存,会设置 main_block_size 的值,其前面的内存空间为 Main block 的区域,之后的每一块区域都由一种名为 BlockHeader 的数据结构描述。 BlockHeader 是一个用于描述每次调用 AllocBlock 所申请到的内存块信息的数据结构,格式如下:

AllockBlock 的内存申请过程是通过 AllocNonvirtualWithoutAccounting 函数实现的,AllocBlock 向它申请了需要的空间大小加上 BlockHeader 大小的空间,之后将所申请到的空间向后偏移了 sizeof(BlockHeader) 从而使得外部使用者得到的空间不包括这一块 BlockHeader 的空间。

在申请完 Tlab 后,Thread 类的对象可直接调用 AllocTlab 传入 size 从 TLAB 中申请指定大小的内存。

从前面可以看出 BumpPointerSpace 作为 TLAB 的设计思路如下图:

  • Heap 类中有一个 bump_pointer_space 的成员变量,指向了一个 BumpPointerSpace 对象,这个对象对应的内存空间可以被任一线程作为 TLAB 使用。
  • 第一个分配 TLAB 的线程会创建一个 Main block,它位于内存资源头部,尾部由 main_block_size 所指明
  • 后续线程 TLAB 均由 BlockHeader 来描述。

Free 与 Clear

Free 函数在 BumpPointerSpace 中直接返回了 0,因为 BumpPointerSpace 的内存分配算法无法实现对指定对象的内存释放

而 Clear 函数的实现如下:

其他实用函数

Walk

Walk 可以遍历内存资源中所包含的 mirror Object 对象,每找到一个都会调用一个名为 ObjectCallback 的回调函数。

Walk 函数会根据 TLAB 的分布方式遍历内存。其中会调用 GetNextObject 函数获取接下来的 Object,它内部实际上就是获取 object + object 的位置上的对象。

GetBytesAllocated

可以通过该函数返回 BumpPointerSpace 分配了多少内存。

RegionSpace

RegionSpace 将内存资源分为了一个个固定大小的内存块(kRegionSize,默认为 1MB),每次进行内存分配时,先找到满足要求的 Region,之后从这个 Region 中分配资源。

RegionSpace 的用法与『拷贝垃圾回收』这种垃圾回收算法有关,它将内存区域分为了两个一半的空间,新对象分配时,所需内存来自其中一个(tospace),另一个作为空闲空间(fromspace)。当 tospace 不够时则会进行垃圾回收,步骤如下

  1. 通过变量交换,将原 fromspace 变为新 tospace,原 tospace 变为新 fromspace
  2. 将新 fromspace 中的存活对象拷贝到 tospace 中
  3. 释放新 fromspace 空间

Create

RegionSpace 的创建过程很容易就能猜到,对传入的 size 对齐到 1MB 的整数倍,然后根据 size 计算创建的 Region 大小,之后创建了一个 region_ 数组,在里面一段段地创建 Region 对象。

同时还要注意 Create 函数中出现的三个参数

  • full_region_ : 一个内存资源不足的内存块
  • current_region_ : 指向当前正在使用的内存块,一开始指向 full_region
  • evac_region_ : 与内存回收相关的 region

Alloc 与 AllocNewTlab

Alloc 实现简述

Alloc 函数的作用主要是为 mirror Object 分配内存。

Alloc 有一个模板参数 kForEvac,它与内存回收算法有关。

首先它会检测要分配的内存大小是否小于 kRegionSize。

比 kRegionSize 小的情况下,若 kForEvac 为 true,它会尝试在当前正在使用的 region 尝试调用 Region::Alloc 函数分配内存,否则会在 evac_region 所指向的 region 尝试调用 Alloc 函数分配内存。若分配成功,则会将对应的对象直接返回。

前面的步骤是没有经过锁同步的,经过上面的过程可能有其他的线程设置了 current_region_,使其指向了其他内存块。所以这里加了一次锁,之后重新进行了一次与前面相同的操作尝试分配内存。(个人认为这样设计是为了加快分配内存的速率)

由于 RegionSpace 用法与拷贝回收算法有关,需要预留一般内存给 fromspace,因此若已被占用内存块个数超过总个数一半,则不再允许分配。

若此时还分配失败,则会尝试遍历内存块,尝试找到一个空闲的内存块,调用 Region::Alloc 函数分配内存后返回。(若 kForEvac 为 true 还会将 evac_region 设置为这个找到的 region。

若要分配的内存大小大于了 kRegionSize,则会调用 AllocLarge 函数进行分配。

AllocLarge 函数的主要作用其实就是使用了多个 region 的空间分配给对象。

Region::Alloc 的主要内存分配实现和 BumpPointerSpace 类似,都是一种从前向后按序的内存分配方式。

也就是说, RegionSpace 主要是将内存分为了一个个大小相等的 Region,而对每个 Region 申请内存时的申请策略都像 BumpPointerSpace 一样按序分配

AllocNewTlab 实现简述

RegionSpace 也可以用作线程的 TLAB,由于 RegionSpace 本身就是按一个个 Region 管理的,因此如果一个线程需要 TLAB 的话,我们只需要找到空闲的 Region 分配给它即可。也就是调用 Thread 的 setTlab 传入 region 的 begin 及 end 即可。

Free 与 Clear

RegionSpace 的内存分配算法也无法支持释放单个 Object 的内存,因此其 Free 方法仍然是空实现,返回了 0。

而对于 Clear,它仅仅是遍历并调用了 region 的 Clear 函数,同时将 current_region_ 及 evac_region_ 设置为了 full_region_。

而 Region 的 Clear 函数也非常简单,主要是对 Region 的一些成员变量进行了重置,并且调用了 madvise 方法对内存进行了置 0。

其他实用函数

Walk

Walk 可以遍历 RegionSpace 中的 Object 对象,每遍历到一个对象都会调用 ObjectCallback 回调函数。它在内部调用了 WalkInternal 函数,这个函数其实做的事情就是遍历 Region,若发现了一个大于 kRegionSize 的大对象,则会获取这个对象调用 callback 进行回调,且对后续的属于这个大对象的 Region 不再进行处理。

而不是这种特殊情况的处理,就会像之前的 BumpPointerSpace 一样,遍历这个 Region 内的所有对象。

RefToRegion

通过 RefToRegion 函数,可以获取到 Object 对象所属的 Region 对象。它内部调用了 RefToRegionLocked 方函数。RefToRegionLocked 函数内部计算了它离 RegionSpace 所在的起始位置有多远,之后用这个偏移量除以 kRegionSize 就可以得到所对应的 Region 在 regions_ 数组中的索引,从而得到对应的 Region。

DlMallocSpace & RosAllocSpace

BumpPointerSpace 及 RegionSpace 都采用了一种比较简单的内存分配算法,这无法满足类似 C 中的 malloc 及 free 这样的比较自由的内存回收及释放的需求。因此 ART 设计了 MallocSpace 并提供了 DlMallocSpace 及 RosAllocSpace 这两个实现类来提供类似 C 中的 malloc 及 free 这样的内存分配及释放功能。

  • DlMallocSpace 使用了开源库 dlmalloc 这一内存分配管理器提供具体的内存分配及释放算法(在一些平台上的 malloc 的底层实现就是用它)。
  • RosAllocSpace 使用了 Google 所开发的 rosalloc 内存分配管理器 ,它的算法比 dlmalloc 复杂得多,且需要 ART 中其他模块的配合,但其分配效果在 ART 虚拟机中比 dlmalloc 更好。

MallocSpace 的创建

通过 HeapCreateMallocSpaceFromMemMap 函数可以创建 MallocSpace 对象。它的代码如下:

可以看出,ART 虚拟机优先是使用 rosalloc 的,其支持一种低内存模式。并且 MallocSpace 所分配的内存是由内存映射技术所提供的,内存的分配与释放都在它上面发生。

DlMallocSpace

DlMallocSpace 内部是使用 dlmalloc 作为内存分配管理器实现的。

Create

在 Create 方法中,它的参数 starting_size 表示初始大小,而 initial_size 表示 dlmalloc 的 limit 水位线。在 dlmalloc 中,它在内部创建了 mspace 作为其内部使用结构,而外部则用 void* 作为其数据类型。

它的 Create 函数首先调用了 CreateMemMap 函数创建了一个 MemMap 对象,之后调用了 CreateFromMemMap 函数将这个 MemMap 传入创建了一个 DlMallocSpace 对象(和 CreateMallocSpaceFromMemMap 调用的方式类似)

在它的 CreateFromMemMap 函数中,首先调用了 CreateMspace 函数创建了一个 mspace 并返回了指向它的指针,之后调用 CHECK_MEMORY_CALL 这个宏检测从而保护了 starting_size 到 capacity 的这段内存。最后调用了 DlMallocSpace 的构造函数并将 mspace 传入。

在 CreateMspace 的实现中,首先调用了 create_mspace_with_base 这个 dlmalloc 的 api 创建了 mspace,之后对其进行判断,若为空指针又调用了 mspace_set_footprint_limit 这一 dlmalloc 的 api 对 mspace 进行设置。最后进行返回。

而在 DlMallocSpace 的构造函数,主要是完成一些成员变量的初始化。

Alloc & Free & Clear & Walk

Alloc

它的 Alloc 主要是由 AllocWithoutGrowthLocked 来实现,其内存分配算法交给了 dlmalloc 来进行处理,调用了其 mspace_malloc 方法进行分配指定大小的内存。但由于 dlmalloc 分配算法的原因,其真实分配的内存大小比 num_bytes 要多,因此此次真实分配的字节数由 AllocationSizeNonvirtual 函数返回(其实也是调用 dlmalloc 的 api 获取相关的信息)

Free

其内存释放的实现就是调用了 mspace_free 这一 dlmalloc 的 api 实现

Clear

Clear 中主要是清零了这个 Space 关联的内存资源,并在上面重新创建了一个 mspace 对象

Walk

MallocSpace 的 Walk 回调函数为 WalkCallback,它所需参数比 ObjectCallback 更多一些,其定义如下:

ObjectCallback 能够通知用户正在遍历哪个 Object,而 MallocCallback 只能通知用户当前遍历的内存段的起始及结束位置,以及该内存段中可被使用的大小。

(从 start 到 end 中有一部分是给内存分配器自己使用的,外部所能够使用的大小由 num_bytes 指示)

它的内部实现也是调用 mspace_inspect_all 这一 dlmalloc 的 api 实现,其传入的回调函数与 WalkCallback 的形式相同,所以可以用同样的函数指针来代表。当遍历结束后还会回调一次 callback(nullptr, nullptr, 0, arg); 通知用户遍历结束。

可以发现,DlMallocSpace 所负责内存分配 释放的核心实现是由 dlmalloc 来完成的。

RosAllocSpace

RosAllocSpace 的实现主要基于 Google 开发的 rosalloc 内存分配管理器

Create

它的 Create 同样也是调用 CreateMemMap 创建了 MemMap 对象,之后将其传入 CreateFromMemMap 函数。

在 CreateFromMemMap 函数中,它先调用了 CreateRosAlloc 函数创建了一个 RosAlloc 对象,之后调用了 RosAllocSpace 的构造函数并传入了这个对象。

在 CreateRosAlloc 函数中,调用了 RosAlloc 的构造函数,但它的最后一个参数会根据是否低内存模式传入不同的值,在低内存模式下传入的是 art::gc::allocator::RosAlloc::kPageReleaseModeAll,否则会传入 art::gc::allocator::RosAlloc::kPageReleaseModeSizeAndEnd

RosAllocSpace 的构造函数主要也是一些成员变量的初始化。

Alloc & Free & Clear & Walk

Alloc

Alloc 主要的实现在 AllocCommon 函数中,它调用了 RosAlloc 对象的 Alloc 方法,并传入了几个变量的引用以获取一些内存分配后的信息(如申请的内存大小,可用内存大小等等)

Free

Free 函数也只是调用了 RosAlloc 的 Free 函数,从而释放内存。

Walk

Walk 主要的实现在 InspectAllRosAlloc 中,这里和一个名为 mutator_lock_ 的锁有关,这个锁是一个 MutatorLock 类型的锁,首先解释一下 Mutator,它与 Collector 是一个相对的词,代表了内存的创建于修改者,也就是我们的应用程序本身。Collector 则代表了内存的回收者。

在 RosAllocSpace 的内存遍历中,它只能在所以线程处于 Suspended 的状态下才能进行遍历。(与 ART 使用 RosAllocSpace 的场景相关)

因此在 InspectAllRosAlloc 函数中,它对三种情况进行了处理

  • 若 mutator_lock 的 IsExclusiveHeld 返回了 true,说明虚拟机已经 suspended,则可以直接调用 RosAlloc 的 InspectAll 进行遍历。
  • mutator_lock 的 IsSharedHeld 返回了 true,说明调用的线程属于 suspended 状态,但其他线程没有,因此会构造一个 ScopedThreadSuspension 对象释放 mutator_lock_ 锁,之后调用 InspectAllRosAllocWithSuspendAll 函数,它会先通过 ThreadList::suspendAll 暂停其他 Java 线程,然后遍历内存,最后恢复线程运行
  • 若都不满足,则直接调用 InspectAllRosAllocWithSuspendAll 函数。

最后,会调用 ThreadList::ResumeAll 恢复被暂停线程的运行。

总结下来就是 RosAllocSpace 的内存遍历需要暂停所有线程


Android Developer in GDUT