Flutter内存分析

简介: 约定: 默认 Android 平台,32位应用 Flutter 版本 1.20 背景 Flutter 接入后,内存的水位升高,oom是较突出的问题。 通过理清以下几个关键问题,可帮助我们更全面认识 Flutter 内存管理,提高解决问题的效率。 Flutter 内存由几部分构成? new space, old space 内存是如何分配,管理的? external 堆内

约定:

  • 默认 Android 平台,32位应用
  • Flutter 版本 1.20

背景

Flutter 接入后,内存的水位升高,oom是较突出的问题。

通过理清以下几个关键问题,可帮助我们更全面认识 Flutter 内存管理,提高解决问题的效率。

  • Flutter 内存由几部分构成?
  • new space, old space 内存是如何分配,管理的?
  • external 堆内存是怎么分配,回收的?
  • gc 触发的入口,时机,条件?

Flutter内存布局

Flutter 内存逻辑上按分配来源可分为4部分:

  • VirtualMemory :Dart Vm内部“内存分配器”实现,通过map/munmap接口获取内存; heap new space , old space 内存分配,释放都经过它。
  • Dart_Handle : Dart Vm 与 外部c/c++ 内存传递的“不透明”指针,里面包含一个heap内对象。external部分内存实际不分配在heap上。
  • map/unmap : engine其他模块直接从系统获取内存,例如skia,gpu等
  • malloc/free : 其他通过标准内存分配器分配的内存

Dart Heap 管理的是 VirtualMemory,external 这2部分内存。

Dart Heap 内存管理

Dart Heap 分代管理内存,新生代gc算法是 Mark-Copying ,老生代gc结合使用 Mark-Sweep, Mark-Compact算法。

Dart Heap 能完全控制 VirtualMemory 部分内存释放,间接控制 external 部分内存的释放(后面描述)。

Dart Vm 对象指针 -- ObjectPtr

ObjectPtr 表示对象在堆中都地址,信息丰富,堆中拷贝,移除,gc发生时遍历被引用对象都通过它进行。

Heap中对象 size 要求是双字(8字节)倍数,因此最低 2 / 3 位可以用来表示其他含义:

  • 0 bit : 是否有效heap对象地址,1 - 有效heap地址,0 表示一个small int,>>1 则可得到数值
  • 2 bit : 对象分布,0 - old generation, 1 - new generation

ObjectPtr 封装对象地址,包含判断有效对象指针,new/old 对象判断等。

ObjectLayout 是所有Dart对象的顶级父类,包含一个Tags对象,实质是一个uint_t,Heap 对象内存模型上都是以Tags对象开始的。Tags按bits分布:

  • class id : 对象类型id
  • size : 对象大小,size位域值 << 3 可计算出;如果超出范围,则通过 ObjectLayout::HeapSizeFromClass() 方法计算,例如一个数组对象
  • gc 辅助信息:存储gc过程中间保存信息

ObjectPtr 与 ObjectLayout 关系

通过 ObjectPtr 可获得对象 类型,大小,新/老 生代,gc 状态信息。那怎么遍历被引用的对象呢?

假设定义一个Dart 类:

class ClassA {
  ClassB _classB;
  ClassC _classC;
}

其在内存中布局示意如下:

    intptr_t instance_size = HeapSize();
    uword obj_addr = ToAddr(this);
    uword from = obj_addr + sizeof(ObjectLayout);
    uword to = obj_addr + instance_size - kWordSize;
    const auto first = reinterpret_cast
  
  
   (from);
    const auto last = reinterpret_cast
   
   
    (to);
   
   
  
  

通过上面简单计算,就可以遍历被引用的 ClassB, ClassC 对象了。Dart gc时候遍历被引用对象用的就是这个方法。

分代内存管理

核心类

Heap 表示Vm的heap,对象分配,释放都是从这里开始,通过Scavenger,PageSpace分别管理 “新生代”,“老生代”内存。

内存分配的核心类是 VirtualMemory,通过封装系统 map/munmap 接口从系统分配大块内存,在“析构” 方法中将内存归还给系统。

最右边部分是gc相关类,Mark-Copying, Mark-Sweep, Mark-Compact 算法具体实现。

新生代内存管理

内存分配

新生代有2个半区:from, to。内存分配都是从to区分配,回收从from区回收。

SmiSpace管理半区内存的分配,涉及几个角色:

  • Thread : Isolate 内部每个线程都会关联一个page,从page中快速分配内存
  • SmiSpace : 以链表结构管理所有分配出来的page;gc就是对该链表中的page进行
  • page_cache : 缓存gc回收的page
  • VirtualMemory : 分配新的page

内存分配步骤如下,成功则不再往下执行:

  1. 从当前线程关联的page中优先分配,空间足够则成功返回
  2. 从SmiSpace管理的page中找一个空闲的page或者空间足够的page进行重新绑定,并进行内存分配
  3. 从page_cache中获取一个新的page,进行分配
  4. 则通过VirtualMemory从系统分配一个新的page

内存分配成功后,会对返回的对象内存进行 tagged 操作,使其满足通过 ObjectPtr 寻址。针对 SmiSpace gc后,释放的page归还到page_cache。

注意点:

  • max_capacity_in_words_ (默认32位8M,64位16M) 管理新生代最大内存,超出范围,则新生代内存分配失败,尝试从老生代进行分配
  • 每个page=512KB,page_cache最大缓存32个page,其余gc时归还给系统
  • 新生代最大对象256KB,大于该值则从老生代分配largePage中分配

新生代gc

SemiSpace* Scavenger::Prologue() {
    ...
    SemiSpace* from = to_;
    to_ = new SemiSpace(NewSizeInWords(from->max_capacity_in_words()));
    ...
}
      

gc 第一步交换 from,to 半区,针对 from 区进行,而gc结束后,除了归还到page_cache缓存中的pages,其他都会随着 from 出栈,析构 方法中释放。新的对象在to区中进行分配。

新生代gc采取代是 Semispace collector 分配器,对象拷贝基于Cheney算法,下面2图描述了算法过程。其主要步骤:

  • 广度搜索优先,拷贝Roots直接引用到to区
  • 拷贝对象到to区时,需要进行forward操作,在from区的旧对象中保存拷贝后to区的新地址;在后续拷贝时,如果有引用到该对象,则需要调整引用地址
  • scan在to区最初始位置,拷贝完Roots后,从scan开始遍历,将to区中对象内部引用的对象进行 回收 或者 拷贝。通过“Dart Vm对象描述“中方法遍历被引用的对象

老生代内存管理

内存分配

老生代内存分配主要角色:

  • PageSpace : 保存所有从 VirtualMemory 中分配过来的page,gc就是对该链表中page进行
  • free_list : 类似内存管理”伙伴系统“算法,将 VirtualMemory 中分配的 page 地址打散,以 16Byte * n 大小分为 128 个链表,例如分配16Byte内存,则直接从第1个链表中返回一段内存地址
  • VirtualMemory :分配新的page

内存分配步骤如下,成功则不再往下:

  1. 通过 size / 16 计算对应落在的区间,如果由空闲空间则分配成功
  2. 尝试从下一级更大内存链表中分配内存
  3. 分配成功,尝试将分配剩余的内存重新放到更小内存区链表中
  4. 不再继续尝试,直接从128最大区中进行内存分配
  5. 同3
  6. 直接从 VirtualMemory 中分配新内存

注意:

  • free_list 中负责 64KB 以下内存分配;更大内存通过 largePage 进行分配,管理较简单,一个page分配给1个对象,gc回收直接使用Mark-Sweep算法。largePage size大小根据需要分配的size而定,并与系统pageSize对齐(4K),可见这种情况特别浪费内存,可能造成比较多的内存碎片。
    intptr_t PageSpace::LargePageSizeInWordsFor(intptr_t size) {
      // 根据需分配size计算,并以4k page对齐
      intptr_t page_size = Utils::RoundUp(size + OldPage::ObjectStartOffset(),
                                          VirtualMemory::PageSize());
      return page_size >> kWordSizeLog2;
    }​
  • 老生代中也会分配 code 缓存,这部分会增加一些权限控制,不细述
  • max_capacity_in_words_ 控制 old space 最大容量,默认 1.5G (30G 64位)

老生代gc

老生代通过Mark-Sweep,Mark-Compact 算法进行内存回收。Sweep 每次回收都会进行,但Compact需要满足一定条件才进行。下图简单描述了算法的过程,其主要步骤:

  • 从Roots深度遍历所有对象,并进行标记
  • 重新计算被标记的对象的拷贝地址,则新地址
  • 遍历对象,如果引用了被标记的对象,需要更新对其的引用地址
  • 拷贝对象

算法的实现细节较多,这里不详细展开。

External 内存

核心类

Dart_Handle

Dart_Handle 可分为3类: LocalHandle 临时本地对象, PersistentHandler,FinalizablePersistentHandle 生存期与isolate同等。每个Handle都有1个 ObjectPtr 对象,这个对象指向的是保存在Dart Heap堆中堆对象。

重点是 FinalizablePersistentHandle ,它有一个指针:void * peer。这个 peer 指向一个在 c/c++ 分配,在Dart Heap 外部的对象。通过 peer 和 ObjectPtr,将这个c/c++对象与Dart Vmd堆heap对象关联起来。如上图中 新生代对象 A,关联的内存实际分配在VM的别处,Dart Heap external 对这种内存的大小进行了统计,但并不由heap来分配。这样做带来的好处是可以将这个 c/c++ 对象的释放托管给Dart Gc,Flutter中典型应用: image ,Layer 等:例如 Image对象,其关联了解码后的c/c++缓存,在Widget销毁的时候,Image对象被回收,c/c++层的解码内存也得到释放。

FinalizablePersistentHandles 也是一类GC Roots,gc的时候,如果其 ObjectPtr 指向的对象没有被标记,则触发回收 peer 指向的对象,本质上是 c/c++ 智能指针引用计数-1操作,如果计数为0才会真正释放 peer 指向的对象。所有这里就会存在释放失败的场景,例如 image 的解码对象被 Handle 引用,同时又被 engine skia或者其他引用了,那在gc的时候,仍然无法释放这个对象,这也是为什么Observatory里面看到image被释放回收了,但内存不一定降下来的原因。

zone

zone 中主要是用于分配一些小对象,这些对象的内存也不从heap中分配,通过Segment直接从系统分配,例如一个获取一个字符串。在gc时机,会对整个zone的内存一起释放。

GC管理

dart_api.h 对外暴露 Dart VM 接口,Flutter通过调用以下接口可触发gc。

/**
 * Notifies the VM that the embedder expects to be idle until |deadline|. The VM
 * may use this time to perform garbage collection or other tasks to avoid
 * delays during execution of Dart code in the future.
 *
 * |deadline| is measured in microseconds against the system's monotonic time.
 * This clock can be accessed via Dart_TimelineGetMicros().
 *
 * Requires there to be a current isolate.
 */
DART_EXPORT void Dart_NotifyIdle(int64_t deadline);

/**
 * Notifies the VM that the system is running low on memory.
 *
 * Does not require a current isolate. Only valid after calling Dart_Initialize.
 */
DART_EXPORT void Dart_NotifyLowMemory();

Dart_NotifyIdle

deadline是传给Dart_NotifyIdle()参数,表示在这个时间限制内完成gc。gc耗时计算方法为:“堆使用字大小 / 每个字gc耗时“。

bool Scavenger::ShouldPerformIdleScavenge(int64_t deadline) {
  ...
  // 计算gc完成后时间点
  int64_t estimated_scavenge_completion =
      OS::GetCurrentMonotonicMicros() +
      used_in_words / scavenge_words_per_micro_;
  // 必须在 deadline 前完成
  return estimated_scavenge_completion <= deadline;
}

scavenge_words_per_micro_ 默认值为 40(根据flutter在Nexus4 上测试获得),后续计算根据最近4次 堆使用字和gc耗时 取平均值。

void Scavenger::Epilogue(SemiSpace* from) {
  ...
  // Update estimate of scavenger speed. This statistic assumes survivorship
  // rates don't change much.
  intptr_t history_used = 0;
  intptr_t history_micros = 0;
  ASSERT(stats_history_.Size() > 0);
  for (intptr_t i = 0; i < stats_history_.Size(); i++) {
    history_used += stats_history_.Get(i).UsedBeforeInWords();
    history_micros += stats_history_.Get(i).DurationMicros();
  }
  if (history_micros == 0) {
    history_micros = 1;
  }
  scavenge_words_per_micro_ = history_used / history_micros;
  ...
}

Dart_NotifyIdle 方法触发的时机有2个:

  • vsync 信号来临,两帧间隔之间触发,deadline 为处理完 BeginFrame() 后到下一帧到来的时间间隔(16ms - BeginFrame耗时)
  • 如果连续3帧时间(51ms)都没有 requestFrame 发出,触发gc,deadline 为 100ms

Heap收到 Dart_NotifyIdle() 信号,需要满足相应的条件才会执行真正的gc操作。条件的判断主要有2个维度:

  • 能够在满足deadline内完成gc操作
  • 是否达到gc条件的内存阀值
    • new space 阀值
      • idle_scavenge_threshold_in_words_ : 与 new_gen_semi_max_size 大小一样,默认 8M(16M 64位)
    • old space 阀值:(old space 阀值包含external部分内存)
      • idle_gc_threshold_in_words_ : 初始化为0,每次gc后重新评估 : "gc后使用内存 + 2* OldPageSize",OldPageSize = 512KB
      • soft_gc_threshold_in_words_ :初始化为0,每次gc后重新评估:
        • 32位与 hard_gc_threshold_in_words_ 相等
        • 64位该值 = hard_gc_threshold_in_words_ - Max( new space /2, hard_gc_threshold_in_words_ /20 )
      • hard_gc_threshold_in_words_ :
        • 依赖配置,在每次gc完成后,重新计算 hard_gc_threshold_in_words_,根据gc回收内存量,满足下面限制下计算新的值
          • garbage_collection_time_ratio_ :FLAG_old_gen_growth_space_ratio,控制gc耗时占比,默认配置3%,例如计算1次gc耗时方式:((本次gc耗时) / (本次gc结束耗时 - 上次gc结束耗时))* 100%
          • heap_growth_max_ :FLAG_old_gen_growth_rat,控制old space 1次最大增大pages数。pageSize = 512KB,默认配置 280
          • desired_utilization_ :1 - FLAG_old_gen_growth_space_ratio,FLAG_old_gen_growth_space_ratio 配置表示每次gc后要求剩余的free空间占比。默认配置为 20%
        • 重新计算策略:
          • 如果自上次gc后,old space 堆上实际使用内存增加,则根据 FLAG_old_gen_growth_space_ratio 条件计算出需要增加的 grow_pages
            • 如果增加使用的内存,且回收的garbage = 0,这时候说明内存需求量较大,则本次增加 growth = max(heap_growth_max_,grow_pages)
            • 如果garbage > 0,说明有垃圾产生,增加内存主要满足 FLAG_old_gen_growth_space_ratio 设置;另外如果 gc耗时超过 garbage_collection_time_ratio_ 的控制,说明 gc 较损耗性能,则适当增加free的空间,分配更多的空间,增大下次gc的阀值,减少整体gc的次数。根据本次产生垃圾的速度,预估下次产生垃圾的量,满足:garbage_collection_time_ratio_ <= 下次垃圾量/old space总大小,计算出一个增量 local_grow_heap,如果 local_grow_heap > heap_growth_max_,则取:growth = max(local_grow_heap, grow_pages),否则 growth = local_grow_heap
          • 如果自上次gc后,old space 堆上实际使用内存没有增加,那条件自上次调整后,依旧满足,growth = 0
      • 最后 hard_gc_threshold_in_words_ = gc后内存占用 + growth * pageSize,每个page 512KB
      • idle_gc_threshold_in_words_ < soft_gc_threshold_in_words_ <= hard_gc_threshold_in_words_

基于上面控制参数,判断流程如下:从上到下是 强->弱 降序排列,gc在满足条件情况下,尽量回收更多的垃圾。

Dart_NotifyLowMemory

如果系统内存过低,可通过embedding FlutterJNI.java 中提供的接口触发:

// shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java
@Keep
public class FlutterJNI {
  ...
  /**
   * Notifies the Dart VM of a low memory event, or that the application is in a state such that now
   * is an appropriate time to free resources, such as going to the background.
   *
   * 

This is distinct from sending a SystemChannel message about low memory, which only notifies * the running Flutter application. */ @UiThread public void notifyLowMemoryWarning() { ensureRunningOnMainThread(); ensureAttachedToNative(); nativeNotifyLowMemoryWarning(nativePlatformViewId); } private native void nativeNotifyLowMemoryWarning(long nativePlatformViewId); ... }

Jni接口注册:

bool RegisterApi(JNIEnv* env) {
    ...
    {
        .name = "nativeNotifyLowMemoryWarning",
        .signature = "(J)V",
        .fnPtr = reinterpret_cast
  
  
   (&NotifyLowMemoryWarning),
    },
    ...
}
  
  

最终在Heap中处理,这时候不会进行条件判断,直接对 new,old space进行垃圾回收

void Heap::CollectMostGarbage(GCReason reason) {
  Thread* thread = Thread::Current();
  CollectNewSpaceGarbage(thread, reason);
  CollectOldSpaceGarbage(
      thread, reason == kLowMemory ? kMarkCompact : kMarkSweep, reason);
}

内部触发

Dart_NotifyIdle(),Dart_NotifyLowMemory() 都是外部调用Dart Vm接口进行的gc,vm内部在内存分配的时候也会进行gc的尝试:

  1. old space 内存分配失败时,会尝试gc,之后再进行内存的分配,再失败,则报oom
  2. 每次分配 external 内存时,new space, old space 都会进行条件的判断,尝试触发gc。
// external 内存,尝试gc
void Heap::AllocatedExternal(intptr_t size, Space space) {
  ASSERT(Thread::Current()->no_safepoint_scope_depth() == 0);
  if (space == kNew) {
    Isolate::Current()->AssertCurrentThreadIsMutator();
    new_space_.AllocatedExternal(size);
    // new space gc条件
    if (new_space_.ExternalInWords() <= (4 * new_space_.CapacityInWords())) {
      return;
    }
    // Attempt to free some external allocation by a scavenge. (If the total
    // remains above the limit, next external alloc will trigger another.)
    CollectGarbage(kScavenge, kExternal);
    // Promotion may have pushed old space over its limit. Fall through for old
    // space GC check.
  } else {
    ASSERT(space == kOld);
    old_space_.AllocatedExternal(size);
  }

  // old space 条件
  if (old_space_.ReachedHardThreshold()) {
    CollectGarbage(kMarkSweep, kExternal);
  } else {
    CheckStartConcurrentMarking(Thread::Current(), kExternal);
  }
}

总结

通过对内存分配来源分析,了解了Flutter内存的全貌。归纳下可分2大部分,一部分是Dart Heap管理,另一部分是Heap外的内存(mmap, malloc(其他内存分配器))。

Dart Heap 的内存关联了 新/老生代Dart对象内存,external部分(Image,Layer 的渲染内存),这些也是Flutter自身内存消耗的主要来源。目前分析主要借助 Observatory 工具,可以观察 Heap 内存增长,gc 的变化。

通过 "persistent handles" 分析 external 内存信息,里面主要是 Image, Layer 相关的内存,"Peer" 是 c/c++ 层的对象指针,Finalizer Callback 是gc回调的方法指针,这里会对 peer 智能指针进行 -1 计数。

Observatory 工具对 Dart Heap 内存的分析还是挺强大的,结合上面对内存梳理的知识,通过灵活应用这个工具,可以帮助我们很好地解决内存泄漏的问题(具体解决问题case,后面再写一篇)。

另外暂时没有对 Heap 进行有效的性能测试:吞吐量,暂停时间,分配速度,使用率。这块可以根据业务场景而优化其性能。

内存问题有时复杂,oom后内存分配具体去哪?这时候对 Dart Heap 外内存的统计对分析,解决问题也会比较有效。

目录
相关文章
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
1188 4
|
5月前
|
存储 弹性计算 缓存
阿里云服务器ECS经济型、通用算力、计算型、通用和内存型选购指南及使用场景分析
本文详细解析阿里云ECS服务器的经济型、通用算力型、计算型、通用型和内存型实例的区别及适用场景,涵盖性能特点、配置比例与实际应用,助你根据业务需求精准选型,提升资源利用率并降低成本。
442 3
|
1月前
|
设计模式 缓存 Java
【JUC】(4)从JMM内存模型的角度来分析CAS并发性问题
本篇文章将从JMM内存模型的角度来分析CAS并发性问题; 内容包含:介绍JMM、CAS、balking犹豫模式、二次检查锁、指令重排问题
106 1
|
4月前
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
249 4
AI代理内存消耗过大?9种优化策略对比分析
|
缓存 监控 前端开发
优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面
本文探讨了优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面,并通过案例分析展示了具体措施和效果,强调了持续优化的重要性及未来优化方向。
521 10
|
7月前
|
数据采集 开发工具 Android开发
ClkLog埋点分析系统-Flutter埋点上报攻略
近期,不少社群里的伙伴有Flutter的集成需求,为了让大家能更快、更顺利地完成集成,我们实现了本次demo给大家作为参考。 目前,我们已为主流的第三方框架提供了相应的集成demo,如果您还有其他SDK的验证需求欢迎联系小秘书,我们会尽量给大家提供实现demo。
ClkLog埋点分析系统-Flutter埋点上报攻略
|
编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(上)
动态内存分配与管理详解(附加笔试题分析)
242 1
|
程序员 编译器 C++
【C++核心】C++内存分区模型分析
这篇文章详细解释了C++程序执行时内存的四个区域:代码区、全局区、栈区和堆区,以及如何在这些区域中分配和释放内存。
186 2
|
8月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。

热门文章

最新文章

下一篇
oss云网关配置