ART内存分配-基于Android S(12)

2 阅读8分钟

前言

本文基于Android 12 源码,通过SDK emulator单步调试分析java Heap 内存分配过程、OOM 判定规则、GC算法,通过表格的形式记录相关参数和简要逻辑,避免长篇大论贴源码带来的枯燥乏味。 我们先将dalvik相关参数放到前面,后面的分析经常用到这几个值:

[dalvik.vm.heapgrowthlimit]: [192m]

[dalvik.vm.heapmaxfree]: [8m]

[dalvik.vm.heapminfree]: [512k]

[dalvik.vm.heapsize]: [512m]

[dalvik.vm.heapstartsize]: [8m]

[dalvik.vm.heaptargetutilization]: [0.75]

Heap内存结构

内存分配器类型

ART中的内存分配器类型定义在allocator_type.h文件中

// Different types of allocators.
// Those marked with * have fast path entrypoints callable from generated code.
enum AllocatorType : char {
  // BumpPointer spaces are currently only used for ZygoteSpace construction.
  kAllocatorTypeBumpPointer,  // Use global CAS-based BumpPointer allocator. (*)
  kAllocatorTypeTLAB,  // Use TLAB allocator within BumpPointer space. (*)
  kAllocatorTypeRosAlloc,  // Use RosAlloc (segregated size, free list) allocator. (*)
  kAllocatorTypeDlMalloc,  // Use dlmalloc (well-known C malloc) allocator. (*)
  kAllocatorTypeNonMoving,  // Special allocator for non moving objects.
  kAllocatorTypeLOS,  // Large object space.
  // The following differ from the BumpPointer allocators primarily in that memory is
  // allocated from multiple regions, instead of a single contiguous space.
  kAllocatorTypeRegion,  // Use CAS-based contiguous bump-pointer allocation within a region. (*)
  kAllocatorTypeRegionTLAB,  // Use region pieces as TLABs. Default for most small objects. (*)
};

不同的分配器对应不同的内存空间,并且有自身的内存分配方式。ART 中所涉及到的内存空间是用SpaceType来表示的,定义在space.h中:

enum SpaceType {
  kSpaceTypeImageSpace,
  kSpaceTypeMallocSpace,
  kSpaceTypeZygoteSpace,
  kSpaceTypeBumpPointerSpace,
  kSpaceTypeLargeObjectSpace,
  kSpaceTypeRegionSpace,
};

Heap初始化参数

heap 初始化时,会创建各种各样的space,通过heap 初始化代码来整理一下,下面是堆初始化的部分参数

成员变量
foreground_collector_type_kCollectorTypeCC
background_collector_type_kCollectorTypeCCBackground
use_homogeneous_space_compaction_for_oom_false
support_homogeneous_space_compactionfalse
separate_non_moving_spacetrue
non_moving_space_capacitykDefaultNonMovingSpaceCapacity = 64 * MB;
heap_reservation_size4k
capacity_512M 即[dalvik.vm.heapsize]: [512m]
large_object_space_typeLargeObjectSpaceType::kMap

内存分配器内存空间对应关系

下面内容基于基于Android 12 SDK emulator,启动一个demo app分析:

内存分配器类型内存空间内存分配函数heap中对应的space 类型(Android 12 现状)memory map中表示
ImageSpaceMemMapImageSpace6f870000-6faca000 rw-p 00000000 00:00 0 [anon:dalvik-/apex/com.android.art/javalib/boot.art]6faca000-6fb0b000 rw-p 00000000 00:00 0 [anon:dalvik-/apex/com.android.art/javalib/boot-core-libart.art]6fb0b000-6fb31000 rw-p 00000000 00:00 0 [anon:dalvik-/apex/com.android.art/javalib/boot-okhttp.art]
kAllocatorTypeBumpPointerBumpPointerSpaceAllocNonvirtual
kAllocatorTypeRosAllocRosAllocSpaceAlloc 或者AllocNonvirtual
kAllocatorTypeDlMallocDlMallocSpaceAlloc 或者AllocNonvirtual"zygote / non moving space"71c14000-7231e000 rw-p 00000000 00:00 0 [anon:dalvik-zygote space] =======> 约7M7231e000-7231f000 rw-p 00000000 00:00 0 [anon:dalvik-non moving space]7231f000-72358000 rw-p 00000000 00:00 0 [anon:dalvik-non moving space]72358000-75415000 ---p 00000000 00:00 0 [anon:dalvik-non moving space]75415000-75c14000 rw-p 00000000 00:00 0 [anon:dalvik-non moving space] =======> 约57M
kAllocatorTypeNonMovingMallocSpaceAlloc
kAllocatorTypeLOSLargeObjectSpaceAlloc"mem map large object space" 默认类型 LargeObjectSpaceType::kMap56c00000-56c04000 rw-p 00000000 00:00 0 [anon:dalvik-large object space allocation]56c04000-56c0c000 rw-p 00000000 00:00 0 [anon:dalvik-large object space allocation]56c0f000-56c3c000 rw-p 00000000 00:00 0 [anon:dalvik-large object space allocation]56c3c000-56c44000 rw-p 00000000 00:00 0 [anon:dalvik-large object space allocation]
kAllocatorTypeRegionRegionSpaceAllocNonvirtual"main space (region space)" 大小 capacity_ * 2 虚拟内存12c00000-52c00000 rw-p 00000000 00:00 0 [anon:dalvik-main space (region space)] =====> 大小1G
kAllocatorTypeTLABBumpPointerSpaceAllocWithNewTLAB 或者AllocTlab
kAllocatorTypeRegionTLABRegionSpaceAllocWithNewTLAB 或者AllocTlab

通过图片直观的感受下:

ART内存分配-基于Android S(12).png

OOM 判定

我们看一下oom的判定函数:

inline bool Heap::IsOutOfMemoryOnAllocation(AllocatorType allocator_type,
                                            size_t alloc_size,
                                            bool grow) {
  size_t old_target = target_footprint_.load(std::memory_order_relaxed);
  while (true) {
    size_t old_allocated = num_bytes_allocated_.load(std::memory_order_relaxed);
    size_t new_footprint = old_allocated + alloc_size;
    // Tests against heap limits are inherently approximate, since multiple allocations may
    // race, and this is not atomic with the allocation.
    if (UNLIKELY(new_footprint <= old_target)) {
      return false;
    } else if (UNLIKELY(new_footprint > growth_limit_)) {
      return true;
    }
    // We are between target_footprint_ and growth_limit_ .
    if (AllocatorMayHaveConcurrentGC(allocator_type) && IsGcConcurrent()) {
      return false;
    } else {
      if (grow) {
        if (target_footprint_.compare_exchange_weak(/*inout ref*/old_target, new_footprint,
                                                    std::memory_order_relaxed)) {
          VlogHeapGrowth(old_target, new_footprint, alloc_size);
          return false;
        }  // else try again.
      } else {
        return true;
      }
    }
  }
}

这里需要解释几个变量的意思和关系

变量意义计算方式
target_footprint_Target size (as in maximum allocatable bytes) for the heap. Weakly enforced as a limit for non-concurrent GC. Used as a guideline for computing concurrent_start_bytes_ in the concurrent GC case.target_footprint_ 是 ART 根据内存使用情况和 GC 策略动态计算的 堆内存目标容量​​,用于指导 GC 触发时机、内存分配失败判断以及堆空间扩展的决策1. 初始化值为[dalvik.vm.heapstartsize]: [8m] 2. 再申请内存时,如果new_footprint在old_target和growth_limit_之间,且grow为true,将target_footprint_更新为new_footprint(增大)3. 在AMS中更新process state,如果满足条件将target_footprint_更新为min_foreground_target_footprint_(减小)
num_bytes_allocated_Number of bytes currently allocated and not yet reclaimed. Includes active TLABS in their entirety, even if they have not yet been parceled out."当前已分配且尚未回收的字节数"(即活动对象占用的内存总量)1. 在AllocObjectWithAllocator函数会将申请的大小增加到num_bytes_allocated_ 2. gc时在函数RecordFree中将freed_bytes减去
growth_limit_he size the heap is limited to. This is initially smaller than capacity, but for largeHeap programs it is "cleared" making it the same as capacity. Only weakly enforced for simultaneous allocations.堆动态软上限1. 初始化值为[dalvik.vm.heapgrowthlimit]: [192m]2. 如果应用在manifest里面指定了largeHeap,则默认设置为[dalvik.vm.heapsize]: [512m] if ((data.appInfo.flags&ApplicationInfo.FLAG_LARGE_HEAP) != 0) {dalvik.system.VMRuntime.getRuntime().clearGrowthLimit();}

综上,oom 判定的条件:

  1. num_bytes_allocated_ + alloc_size <= target_footprint_ 正常申请
  2. num_bytes_allocated_ + alloc_size > growth_limit_ OOM
  3. num_bytes_allocated_ + alloc_size 在target_footprint_ 和growth_limit_ 之间有以下三种情况:
  • 满足AllocatorMayHaveConcurrentGC和IsGcConcurrent函数均为true,正常申请(Android12满足)
  • 如不满足AllocatorMayHaveConcurrentGC和IsGcConcurrent函数均为true,且指定了grow为true,更新target_footprint_,正常申请
  • 如不满足AllocatorMayHaveConcurrentGC和IsGcConcurrent函数均为true,且指定了grow为false,OOM

largeHeap 对应用的影响

androd 应用可以在manifest里面指定是否是largeHeap 应用,如下代码, 下面是谷歌官方的解释:

android:largeHeap="true"

截图 2025-05-16 09-21-45.png

如果应用在manifest中指定了largeHeap,PMS在进行包扫描的时候就会给app的applicationinfo中增加下面这个flag

    /**
     * Value for {@link #flags}: true when the application has requested a
     * large heap for its processes.  Corresponds to
     * {@link android.R.styleable#AndroidManifestApplication_largeHeap
     * android:largeHeap}.
     */
    public static final int FLAG_LARGE_HEAP = 1<<20;

在handleBindApplication函数中会对这个flag有特殊处理:

        if ((data.appInfo.flags&ApplicationInfo.FLAG_LARGE_HEAP) != 0) {
            dalvik.system.VMRuntime.getRuntime().clearGrowthLimit();
        } else {
            // Small heap, clamp to the current growth limit and let the heap release
            // pages after the growth limit to the non growth limit capacity. b/18387825
            dalvik.system.VMRuntime.getRuntime().clampGrowthLimit();
        }

最终调用ART 中heap.cc中的ClearGrowthLimit 函数,这个重点将growth_limit_值调整, 上面的介绍中我们知道该值为192M,会更新为512M,

void Heap::ClearGrowthLimit() {
  if (target_footprint_.load(std::memory_order_relaxed) == growth_limit_
      && growth_limit_ < capacity_) {
    target_footprint_.store(capacity_, std::memory_order_relaxed);
    concurrent_start_bytes_ =
        UnsignedDifference(capacity_, kMinConcurrentRemainingBytes);
  }
  growth_limit_ = capacity_;
  ScopedObjectAccess soa(Thread::Current());
  for (const auto& space : continuous_spaces_) {
    if (space->IsMallocSpace()) {
      gc::space::MallocSpace* malloc_space = space->AsMallocSpace();
      malloc_space->ClearGrowthLimit();
      malloc_space->SetFootprintLimit(malloc_space->Capacity());
    }
  }
  // This space isn't added for performance reasons.
  if (main_space_backup_.get() != nullptr) {
    main_space_backup_->ClearGrowthLimit();
    main_space_backup_->SetFootprintLimit(main_space_backup_->Capacity());
  }
}

结论:

系统设置应用设置堆上限
[dalvik.vm.heapgrowthlimit]: [192m][dalvik.vm.heapsize]: [512m]android:largeHeap=true512m
仅设置[dalvik.vm.heapsize]: [512m]应用是否设置android:largeHeap=true 无影响,堆大小都是512m if (args.GetOrDefault(M::HeapGrowthLimit) <= 0u 或 args.GetOrDefault(M::HeapGrowthLimit) > args.GetOrDefault(M::MemoryMaximumSize)) {args.Set(M::HeapGrowthLimit, args.GetOrDefault(M::MemoryMaximumSize));}512m
[dalvik.vm.heapgrowthlimit]: [192m][dalvik.vm.heapsize]: [512m]android:largeHeap=false192m

GC算法

GC的判断条件

在内存分配器申请内存的函数AllocObjectWithAllocator中,每次都会调用ShouldConcurrentGCForJava,这里面影响gc的因素只有两个值,我们从这两个值入手,解释下GC的算法

inline bool Heap::ShouldConcurrentGCForJava(size_t new_num_bytes_allocated) {
  // For a Java allocation, we only check whether the number of Java allocated bytes excceeds a
  // threshold. By not considering native allocation here, we (a) ensure that Java heap bounds are
  // maintained, and (b) reduce the cost of the check here.
  return new_num_bytes_allocated >= concurrent_start_bytes_;
}

new_num_bytes_allocated

new_num_bytes_allocated的计算在函数AllocObjectWithAllocator中,该函数是申请内存的入口函数:

fen.png

concurrent_start_bytes_

每次GC的时候都会更新下一次GC的水位线concurrent_start_bytes_,入口在CollectGarbageInternal函数中,水位线的调整在函数中,为了便于记忆和理解,我们使用demo app在Adnroid 12 模拟器上单步调试分析,由于影响因素太多我们将其中涉及到的一些变量还是表格化显示,便于记忆和理解:

gc.png

小结

好了我们总结一下gc水位线的计算过程

  • 首先计算grow_bytes,根据heaptargetutilization计算出在目前的bytes_allocated的基础上需要增加多少(delta),要确保grow_bytes落在[512k,8m]中间
  • target_size 计算,target_size等于grow_bytes乘以multiplier,后台1,前台3,目的是为了降低前台时的gc 频率,计算出来之后将target_size 通过函数SetIdealFootprint赋值给target_footprint_
  • remaining_bytes计算,remaining_bytes是预留给gc期间申请内存用的,因为目前的gc是并发copy,remaining_bytes要保证落在[128k,512k]之间
  • gc 水位线计算:concurrent_start_bytes_ = std::max(target_footprint - remaining_bytes, bytes_allocated)