天天看點

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

約定:

  • 預設 Android 平台,32位應用
  • Flutter 版本 1.20

背景

Flutter 接入後,記憶體的水位升高,oom是較突出的問題。

通過理清以下幾個關鍵問題,可幫助我們更全面認識 Flutter 記憶體管理,提高解決問題的效率。

  • Flutter 記憶體由幾部分構成?
  • new space, old space 記憶體是如何配置設定,管理的?
  • external 堆記憶體是怎麼配置設定,回收的?
  • gc 觸發的入口,時機,條件?

Flutter記憶體布局

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

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
Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

ObjectPtr 封裝對象位址,包含判斷有效對象指針,new/old 對象判斷等。

ObjectLayout 是所有Dart對象的頂級父類,包含一個Tags對象,實質是一個uint_t,Heap 對象記憶體模型上都是以Tags對象開始的。Tags按bits分布:

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結
  • class id : 對象類型id
  • size : 對象大小,size位域值 << 3 可計算出;如果超出範圍,則通過 ObjectLayout::HeapSizeFromClass() 方法計算,例如一個數組對象
  • gc 輔助資訊:存儲gc過程中間儲存資訊

ObjectPtr 與 ObjectLayout 關系

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

通過 ObjectPtr 可獲得對象 類型,大小,新/老 生代,gc 狀态資訊。那怎麼周遊被引用的對象呢?

假設定義一個Dart 類:

class ClassA {
  ClassB _classB;
  ClassC _classC;
}           

其在記憶體中布局示意如下:

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結
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時候周遊被引用對象用的就是這個方法。

分代記憶體管理

核心類

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

Heap 表示Vm的heap,對象配置設定,釋放都是從這裡開始,通過Scavenger,PageSpace分别管理 “新生代”,“老生代”記憶體。

記憶體配置設定的核心類是 VirtualMemory,通過封裝系統 map/munmap 接口從系統配置設定大塊記憶體,在“析構” 方法中将記憶體歸還給系統。

最右邊部分是gc相關類,Mark-Copying, Mark-Sweep, Mark-Compact 算法具體實作。

新生代記憶體管理

記憶體配置設定

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

新生代有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對象描述“中方法周遊被引用的對象
Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結
Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

老生代記憶體管理

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

老生代記憶體配置設定主要角色:

  • 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深度周遊所有對象,并進行标記
  • 重新計算被标記的對象的拷貝位址,則新位址
  • 周遊對象,如果引用了被标記的對象,需要更新對其的引用位址
  • 拷貝對象

算法的實作細節較多,這裡不詳細展開。

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

External 記憶體

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

Dart_Handle

Dart_Handle 可分為3類: LocalHandle 臨時本地對象, PersistentHandler,FinalizablePersistentHandle 生存期與isolate同等。每個Handle都有1個 ObjectPtr 對象,這個對象指向的是儲存在Dart Heap堆中堆對象。

Flutter記憶體分析背景Flutter記憶體布局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在滿足條件情況下,盡量回收更多的垃圾。

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

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 的變化。

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

通過 "persistent handles" 分析 external 記憶體資訊,裡面主要是 Image, Layer 相關的記憶體,"Peer" 是 c/c++ 層的對象指針,Finalizer Callback 是gc回調的方法指針,這裡會對 peer 智能指針進行 -1 計數。

Flutter記憶體分析背景Flutter記憶體布局Dart Heap 記憶體管理總結

Observatory 工具對 Dart Heap 記憶體的分析還是挺強大的,結合上面對記憶體梳理的知識,通過靈活應用這個工具,可以幫助我們很好地解決記憶體洩漏的問題(具體解決問題case,後面再寫一篇)。

另外暫時沒有對 Heap 進行有效的性能測試:吞吐量,暫停時間,配置設定速度,使用率。這塊可以根據業務場景而優化其性能。

記憶體問題有時複雜,oom後記憶體配置設定具體去哪?這時候對 Dart Heap 外記憶體的統計對分析,解決問題也會比較有效。