Flutter内存分析

约定:

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

背景

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

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

  • Flutter 内存B ! O - b @ J由几部分构成?
  • new spacN H .e, old space 内存是如何分A k H z % $ H ; J配,管理的?
  • external 堆内存是怎么分配,回收的?
  • gc 触发= w E 1 ) $的入口,时机,条件?

Flutter内存布局

Flutter内存分析

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

  • VirtualMemory :Dart V` F ?m内部“内存分配器”实现,通过map/munmap接口获取内存; heap new space , o( v e n m ald space 内存分配,释放都经过它。
  • Dart_Handle : Dart Vm 与 外部c/c++ 内存传递的“不透明”指针,里面包含一个hr U ) s @ dea8 x Lp内对象。external部分内存实际不分配在hd 4 P N @ 9 : D Seap上。
  • map/unmap : engine其他模块直接从系统获取内存,例如skia,gpu等
  • malloc/free : 其他通过标准内存分配器分配的内存

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

Dart Heap 内存管理

Da2 = N T ] brt Heap 分代管理内存,新生代gc算法是 Mark-Copying ,老生代gc结合使用 Mark-Sweep, Mark-Compact算法

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

Dart Vm 对象指针 -- ObjectPtr

ObjectPtr 表示对象在^ g t F d k N堆中都地址,信息丰富,堆中拷贝,移除,gc发生时遍历被引用对象都* - K 1 8 w = j通过它进行。

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

  • 0 bit : 是否有效heap对象地址,1 - 有效heap地址4 9 : $ x D I,0 表示一个small int,>>1 则可得到数值
  • 2 bit : 对象分布,0 - old generation, 1 - newQ n j - G ` 4 6 generation

Flutter内存分析

ObjectPt; r i 8 7r 封装对象地址,包含判断f y J /有效对象指针,new/old 对象判断等。

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

Flutter内存分析

  • class id : 对象类型idp P k + P W .
  • size : 对象大小,size位域值 << 3 可计算出;如果超出范围,则通过 Object] t ` Q 3 # , [ LLayout::HeapSizeFr8 G 8 f . domClass() 方法计算,例如一个数组对象
  • gc 辅助信息:存储gc过程中间保存信息

ObjectPtr 与 ObjectLayout 关系

Flutter内存分析

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

假设定义一个Dart 类:

class ClassA {
ClassB _classB;
ClassC _classC;
}

t G A d V在内存中布局示意如下:

Flutter内存分析

    intptr_t instance_size = HeapSize();
uword obj_addr = ToAddr(this);
uword from = obj_addr + siX D & ; J @ 1 I Kzeof(ObjectLayout);
uworO ; - {d to = obj_addr + instance_size - kWordSize;
const auto first = reinterpret_cast&% m Llt;ObjectPtr*&g@ N g dt;(from);
constg i F x v ^ l v x auto last = reinterpret_cast<ObjectPtr*>(to);

通过? 9 N P ] p 0 -上面简单计算,就可以遍历被引用的 ClassB, ClassC 对象了。Dart gc时候遍历被引用对象用的就是这个5 O L方法。

分代内存管理

核心类

Flutter内存分析

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

内存分配的核心类是 VirtualMeY } + k , m X U Smory,通过封装系统 map/munmap 接口从系统分配大r S R c d块内存,在“析构”F z w i % u 方法中将内存归还给系统。

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

新生代内存管理

内存分配

Flutter内存分析

新生代有2个半区:from, to。内l Y ( V C 2 h存分配都是从to区分配,回收从from区回收。

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

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

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

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

内存分配成功后,会对返回的对象内存进行 tagg 7 u + d e | }ed 操作,使其满足通过 ObjectPtr 寻址。针对O j 7 M V + % g i SmiSpace gc后,释放的page归还到page_cache。

注意点:

  • max_capacity_in_words_ 7 ; g 6 | C 1_ (默认32位8M,64位16M) 管理新生代最大内存b | 2 R D ^,超出范围,则新生代内存分配失败,尝试从老生代进S D N行分配
  • 每个page=512KB,page_cache最大缓存32个page,其余gc时归还给系统
  • 新生代最大对象X . # c ` d r z R256KB,大于该值则从老生代分配largePage中分配

新生代gc

SemiSpace* Scavenger:Q f ! R } l !:Prologue() {
...
SemiSpace* from = to_;
to_ = new SemiSpace(NewSizeInWords(from->max_capacity_in_words()));
...
}

gc 第一L 7 P a k ( ^ &步交换from,to 半区,针对 from 区进行~ P B L P s } n ~,而gc结束后,除了7 * d归还到page_cache缓存中的pages,其他都会随着 from 出栈,析构 方法中释放。新的对象在to区中进行分配。

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

  • 广度搜索优先,拷贝Roots直接引用到to区
  • 拷贝对象到= X Qto区时,需要进行forward操作,在from区的旧对: ( Z Q b象中保存拷贝后to区的新地址;在后续拷贝时,如果有引用到该对象,则需要调整引用地址
  • scan在to区最初始位置,+ Q ^ i / I F拷贝完RS v 8oots后,从scan开始遍历,将to区中对象内部引用的对象进行 回收 或者 拷贝。通过“Dart Vm对象描述“c = 5 ; k C +中方法遍历被引用的对象

Flutter内存分析

Flutter内存分析

老生代内存管理

内存分配

Flutter内存分析

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

  • PageSpace : 保存所有从 VirtualMemory 中分配过来的page,gc就是对该链表中pagee ; {进行
  • free_list : 类似内存管理”伙伴系统j h v v - E r /“算法,将 VirtualMemos # @ 4 ^ & % &ry 中分配的 page 地址打散,以 16Byte * n 大小分为 128 个链表,例如分配16Byte内存,则直接从第1个链表中返回一段内存地n @ a T = 3 p
  • VirtualMemory :分配新的page

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

  1. 通过 size / 16 计算对应落在的区间,如果由空闲空间则分配成功
  2. 尝试从下一级更大内存链表中分配内存
  3. 分配成功,尝试将分配剩余& 5 % 4 P ! E的内存重新放到更小g K m E Y内存区链表中M F 0
  4. 不再继续尝试,直接从128最大R E T R k t * i l区中进行内存分配
  5. 同3
  6. 直接从 VV { B z jirtualMemory 中分配新内存

注意:

  • frz - n l a + { M :eE b U se_list 中负责 64KB 以下内存分配;更大内存通过 lar* ( 6 %gePage 进行分配,管理较简单,一个page分配给: { o 0 J1个对象,gc回收直接使用Mark-Sweep算法。largePage size大小根据需要分配的size而定,并与系统pageSize对齐(4Kb n ~ n R),可见这种情况特别浪费内存,可能造成比较多的内存碎片。
    intptr_* m w N V  e Kt PageSpace::LargePageSizeInWordsFor(intptr_t size) {
    // 根据需分配size计算,并= w m m以4k page对齐
    intptr_t page_size = Util[ ( 4 E B @ V *s::RoundUp(size + OldPage::Obr 7 O R : C vjec[ W Q C ] }tStartOffset(),
    VirtualMemory::PageSize());
    return page_[ q D N z 2 X Ysize >> kWordSizeLog2;
    }​
  • 老生代中也会分配 code 缓存,这部分会增加一些权限控制,不细述
  • max_capah 6 I Z W # f h city_in_words_ 控制 old sx . ^pace 最大容量,默认 1.5G (30G 64位)

老生代gc

老生代通过Mark-Sweep,Mark-Compact 算法进行内; S i z 8 S 4存回收。Sweep 每次回收都会进行,但CompN ) y = ^ j gact需要满足一定条P 6 2 O Z件才进行。下图简单描述了算法的过程,其主要步骤:

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

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

Flutter内存分析

Externag Y 0l 内存

核心类

Flutter内存分析

Dart_Handle

Dart_HaU # z E X B undle 可分为3类: Local= R | * q nHandle 临时本地对象, PersistentHandler,Finalizab/ ` $ { G ` x | NlePersistentHaI e l & 4 Cndle 生存期与isolate同等。每u u Z w U / F v f个Handle都有1个 ObjectPtr 对象,这个对象指向的是保存在Dart Heap堆中堆对象。

Flutter内存分析

重点是 Fi? [ 2 5 Y k 1 s %nalizaK u IblePersistentHandle ,它有一个指针:void * pe/ L ? C ] 4 x 4er。这个 peer 指向一个在 c/c++ 分配,在Dart Heap 外部的对象。通过 peer 和E Z 6 ObjectPtr,将这个c/c++对象与Dart Vmd堆heap对象关联起来。如u 1 3 k y N上图中 新生代对象 A,关联的` w & | 9 v k l `内存实际分配在VM的别处,Dart Heap external 对这种内存的大小进s f } ~ `行了统计,但并不由heap来分配。这样做带来的好处是可以将这个 c/c++ 对象的释放托8 F ( z t 9 : N P管给DarH v g ] St Gc,Flutter= R 2中典型应用: image ,Layer 等:例如 Image对象,其关联了解码K l s j 2 X 7 s后的c/c++缓存,在Widget销毁的时候,Image对象+ f , l B Y H d被回收,c/c++层的解码内存也得到释放。

Finalizabl. p ^ V , U A ^ePersistentHandles 也是一类GC Roots,gc的时候,如果其 ObjectPtr 指向的对象没有被标记,则触发回E E 1 t收 peer 指向的对象,本质上是 c/T 1 Y * f @ a l 9c++ 智能指针引用计数-1操作,如果计数t ^ * H - r 2 I }为0才会真正释放 peer 指向的对象。所有这里就会存在释放失败的场景,例如~ u e a @ ~ 3 image 的解码对象被 Handle 引用,同时又被 engine skia或者其他引用了,那Y } h在gc) I U ] b L的时候,仍然无法释放这个对象,这也l 8 & k K L是为什么Observatory里面看到image被释放回收了,但内存不一定降下来的原因。

zone

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

GC管理

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

/**
* Notifies th3 i Q q g E Fe VM that the embedder expects t8 $ W m M % Io 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| i{ 5 Ks 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 runniT b r T : # S xng low on memory.
*
* Does not require a current isolate. Only v~ w K 6 z ,alid after calling Dart_InQ D H D P titialize.
*/
DART_EXPORT void Dart_NotifyLowMemory();

Dart_NotifyIdle

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

bool Scavenger::ShouldPerformIdleScavenge(int64_t deadline) {
...
// 计算gc完成后时间点
int~ ( ]64_t esK { Vtimated_scavenge_completion =
OS::GetCurrentMonotonicMicros() +
used_in_} . cwords / scavenge_words_per_micro_;
// 必须在 deadline 前完成| T p c  m T 4 (
return estimated_scavenge_completion <= deadline;
}

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

void Scavenger::Epilogue(SemiSpace* frP K G )om) {
...
// Update estimate of scavenger spep { % ted. TF G p 2 ihis statistic assumes survivorship
// rac / ~ | / ;  - Mtes don't change much.
intptr_t history_used = 0;
intptr_t history_micros = 0;
ASSERT(staX I x H m u O j {ts_history_.Size() > 0);
for (intptr_ti $ 5 & I P i = 0; i < stats_history_.Size(); i++) {
history_used += stats_history_.Get(i).UsedBeforeInWords();
history_micros += stats_history_I z q.Get(i).Duratn z k wionMicros();
}
if (history_micros == 0) {
history_micros = 1;
}
scavenge_words_per_micro_ = history_used / history_micros;
...
}

Da[ 9 U H Z 4 Mrt_NotifyIdle 方法触发的时机有2个:

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

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

  • 能够在满足deaB 1 J x s Y [dline内完成gc操作
  • Y T u否达到gc条% 5 h a i i y q 7件的内存阀值
    • new space 阀值
      • idle_scG 4 . Q ; u Q s pavenge_threshold_in_words_ : 与 new_gen_semi_max_siz? N h He 大小一样,默认 8M(16M 64位)
    • old sp$ 9 ! Xace 阀值:(old space 阀值包含external部分内存)
      • idle_gc_threshold_in_words_ : 初始化为0,每次gc后重新评估 : "gc后使用内存 + 2* OldPageSize",OldPageSize = 512KB
      • soft_gc_threshold_? , W =in_words_ :初始化为0,每次gc后重n 8 d D W新评估:
        • 32位与 hard_gc_threshold_in_words_ 相2 9 l y 6 *
        • 64位该值 =G ^ * m # f hard_gc_threshold_in_words_ - Max( new space /2, hard_gc_threshold_in_words_ /20 )f U ~ K & A
      • hard_gc_threshold_in_words_ :
        • 依赖配置,在每次gc完成后,重新计算 hard_gc_threshold_in_words_,根据gc回收内存量,满足下面限制下计算新的值
          • garbage_collection_time_ratio_ :FLAG_old_gen_growth_sp+ Q P ` O f Z G +ace_ratio,控制gc耗时占比,默认配置3%,例如计算1次gc耗时方式:((本次gc耗时) / (本次gc结束耗时 - 上次gc结束耗时))* 100%
          • heap_growth_max_ :FLAJ r Y G E X ( ; MG_old_gen_growth_rat,控制old space2 $ # 1次最大增大pages数。pageSize = 512KB,默认配置 280
          • desired_uti_ % Clization_ :1 - FLAZ W 4G_old_gen_growth_sK 5 * epace_ratio,FLAG_old_gen_L _ h fgr/ p S ` q %owth_space_ratio 配置表示每次gc后要求剩余的free空间占比。默认配置为 20%
        • 重新计算策略:
          • 如果/ t s L _ X k自上次gc后,old space 堆上实际使用内存增加,则根据 FLAG_old_gen_growth_space_ratio 条件计算出需要增加/ D | ! @ A的 grow_pages
            • 如果增加使用的内存,且回收的garbage = 0,这时候说明内存需求量较大,则本次增加 growth = max(heap_growtu m 1 * A 2 eh_max_,grow_pages)
            • 如果garbage > 0,说明有垃圾产生,增加内存主要满足 FLAG_old_gen_growth_space_ratio 设置;另外如果 gc耗时超过 garbage_collection_time_ratio_ 的控制,说明 gc 较损耗性能,则适当增加free- N a k T _ v的空间,分配更多的空间,增大下次gc, . H ` B [ 4的阀值,减少整体gc的次数。根据本次产生垃圾的速度,预估下次产生垃圾的量,满足:garbage_collectionm i Z l _ 5 n %_^ V , t L [ |time_ratio_ <= 下次垃圾量/old space总大小,计算出一个增量 local_grow_heap,如果 locax b J rl_gro} ! ~w_heap > heap_growth_max_,则取:growth = max(local_grow_heap, grow_pages),否则 growth = local_grow_heap
          • 如果自上次gc后,old space 堆上实际使用内存r # `没有增加,那条件自上次调整后,依旧满足,growth = 0
      • 最后 hard_gc_threshold_in_words_ = gc后内存占用 + growth * pageSize,每个page 512KB
      • idle_gp , X ;c_threshold_in_words_ < soft_gc_threshold_in_words_ &lw 8 a & ;t;=hard_gc_threshold_in_words_

基于上面控制参数,判断q l F @流程如下:从上到下是} = a / ` 强->弱 降序排~ A ] } j N { s列,gc在满足条件情况下,尽量回收更多的垃圾。

Flutter内存分析

Dart_Noti. U : h ] ] sfyd h lLowMemory

如果系统内存过低,可通过embedding FluttK + u ; rerJNI.java 中提供的接口触发:

// shell/platform/android/io/flutte, M vr/embedding/X B H 5 g + ` UenP p hgine/FlutterJNI.java
@Keep
public class FlutterJNI {
...@ n } S  P
/**
* Notifies the Dart VM of a low memory event,# ) E # E ] ? k or that the application is in a state such that nou F O f ~ q 9w
* is an appropriate time to free resources, such as going to the background.
*
* <p>This is distinct from sending a SG * +ystemChann, [ D m @ Q 7 2el message about low memory, which only notifies
* the runn? T O $ Eing Flutter application.
*/
@UiThread
public void notifyLow# 5 & x 7 lMemoryWarning() {
ensureRunningO| G | d = y b +nMainThread();
ensureAttachedTb & j i roNative();
nativeNotifyLowMemoryWarnk m M f |ing(nativePlatformG + Q v ; 6ViewId);
}
private native void nativeNotifyLowMemoryWarning(long nativePlatformViewId);
...
}

Jni接口注册:

bool RegisterApi(JNIEnv* env) {
...
{
.name = "nativeNotifyLowMemoryWarning",
.signaturev 9 s K =l + V $ = "(J)V",
.fnPtr = reinterpret_cast<void*>X l );(&NotifyR T ; OLowMemoryWarni5 m xng),
},
...
}

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

void Heap::CollectMostGarbage(GCReason reason) {
ThreM t oad* thread = Thread::Current();
Colle! t Q _ B |ctNewSpaceGarbage(thread, reason);
CollectOldSpaceGar) ~ - J 5 z j p 1bag9 h H C ie(
thread, reason == kLowMemory ? kMarkCompact :g ` S z I E kMarkR S J T CSweep, reason);
}

内部触发

Dart_NotifyIdle(% $ G v p T R ,),) ] 9Dart_NotL K 6 j t D IifyLow& { - ^ f UMeme ^ { [ p c t ) .ory() 都是外部调用Dart Vm接口进行的gc,vm内部在内存分配的时候也会进 ` j s E 6行gc的尝试:

  1. old. d ) j A f q space 内存分配失败时,会尝试gc,之后再进行内存的分配,再失败,则报oom
  2. 每次分配 external 内存时,new space, old space 都会进行条件的判断,尝6 k V # V [ 9 V试触发gc。
// external 内存,尝试gc
void Heap::Allocated8 b k T %External(intptr_t size, Space space) {
ASSERT(Thr| p B ( O N A o Keav c W 8 4 L Cd::Current()->no_safepoint_scope_depth() == 0);
if (space == kNew) {
Isolate::Current()->AssertCurrentThr g $ 2 [ j L QreadIsMutator();
new_space_.AllocatedExternal(size);
// new space gc条件
if (new_space( [ p g = X G_.ExternalInWords() <= (4 * new_space= e ! : w O_.CapacityInWords())) {
returnX d Z j ( + T b {;
}
// Attempt t/ 2 9 y h - N S oo free some eN # E x u /xternal allocation by a scavenge. (If. X B  A the total
// remains above the limit, next external alloc will trigger anothe[ ] 5 o A O v C rr.)
CollectGarbage(kScavenge, kEB Y (xternal);
// Promotion may have pusX C ehed old space over its limit. Fall through for old
// space GC check.] 6 ` h ~ ^ v 5
} else {
ASSERT(space =t ? } B R 6 G |= kOld);
old_space_.AllocatedExternal(size);
}
// old space 条件
if (old_space_.ReachedHar5 6 f w s l  , PdThreshold()) {
CollectGarbage(kMarkSweep, kExternal);
} else {
CheckStartConcurJ ( trentMarking(Thread::Current(k ] t g n O 8 Y I), kExternY - , L _ Dal);
}
}

总结

通过对内存分配来源分析,了解了Flutter内存的全貌。归纳下可分2大部分,一部分是Dae H A t : m & i *rt Heap管理,另一部分是Heap外的内存(mmap, ma. a 4 | i 4 u X *lloc(其他内存分配器))。

Dart Heap 的内存关联了 新/老生代Dart对象内存,external部分(Image,LP ~ , u G b f :ayer 的渲染内存),这些也是Flutter自@ 7 e - 4 B M身内存消耗的主要来源。目前分析主要借助 Observatory 工具,可以观察 HeN T 3 uap 内存增长,gc 的变化。

Flutter内存分析

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

Flutter内存分析

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

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

g i L 1 Q ? %存问题有& 6 B V时复杂,oom后内存分配具体去哪?这时候对 Dart Heap 外内存的统计对分析,解决问题也会比较有效。