sponsored links

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

本文描述的虛擬機器記憶體管理最佳化方案,是從應用側視角對 Android 虛擬機器記憶體管理進行改造,優化了虛擬機器對 LargeObjectSpace 的記憶體管理策略,間接增加其它記憶體空間使用上限。改造後的方案,32 位執行環境 LargeObjectSpace 的記憶體使用上限可達到 2G 甚至更多(64 位環境使用上限理論上會趨於無限大)。透過本方案可以最大程度上從系統側解決諸多應用都會遇到的記憶體瓶頸和 OOM 問題,一鍵接入,安全可靠。

1.背景

Java OOM對於 Android 開發者來說並不陌生,隨著應用愈發複雜,部分業務在設計之初為了更好的產品體驗,往往會考慮用空間換時間,長此以往,有限的記憶體資源將會不堪重負,尤其是在一些重大活動期間,記憶體挑戰會更加嚴峻,稍有不慎就會暴雷。

為了應對這種情況,開發同學往往會從應用層面進行最佳化,減少快取,但隨著產品持續迭代,記憶體問題始終如達摩克利斯之劍,讓應用處於崩潰的邊緣。好在 Android 系統維護人員也意識到了這種問題,從 Android O 系統開始,對 Java 記憶體管理策略進行了調整,重點包括例項化 Bitmap 物件時,Bitmap 源資料的記憶體不再透過虛擬機器申請,而是直接在 Native 層申請和管理,因此這部分記憶體不會納入虛擬機器 Heap 記憶體統計,以此來減少 LargeSpace 的佔用,進而間接增加了其他記憶體空間的實際使用範圍(此消彼長的關係),如下示意圖:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

透過調整 Bitmap 的記憶體管理策略以減少虛擬機器整體記憶體使用,的確帶來了立竿見影的效果。以位元組公司內部諸多 App 為例,Android O 之後的移動裝置,Java OOM 遠遠低於早期的系統版本。

2.思考與破局

對於上述 Android 記憶體管理策略最佳化,可以看到其實只有 Android N 以上版本受益,但是市面上仍有大量的低版本裝置需要關注,為了帶給使用者更好的體驗,我們開始思考,既然在 Android O 系統上可以透過調整 Bitmap 記憶體管理策略,以降低 Java 記憶體觸頂壓力,那麼針對 Android 低版本是不是也可以考慮透過同樣的方式去轉移記憶體統計策略呢?

經過一番探索,最終找到了我們想要的答案:在應用側,我們實現了對虛擬機器記憶體管理策略的改造,將 LOS(Large Object Space)的整個記憶體使用,從虛擬機器的記憶體統計之中進行移除,以保障其它記憶體 Space 可以更大範圍地申請記憶體。

3.背景知識介紹

在正式介紹該方案之前,有必要先來簡單瞭解一下 Android 虛擬機器的記憶體空間管理、Java 大物件管理以及記憶體申請流程,以便於我們更加清晰地理解該方案的設計思想。

3.1 記憶體空間分類

眾所周知,系統在建立 Zygote 過程中會初始化虛擬機器配置,其中一個便是設定 Heap 記憶體。預設 HeapMax 是 256M、512M,後續應用程序透過 Zygote 程序孵化時,都會繼承該配置且無法修改。但對於虛擬機器來說,為了更好地管理記憶體並提升分配和回收效能,並沒有將所有 Java 物件全部放在一塊空間進行管理,而是按照不同的場景屬性劃分成若干個記憶體空間,這些記憶體空間將會共享虛擬機器 512M 最大記憶體,因此它們之間是一個此消彼長的關係,如下圖:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

同時虛擬機器在 GC 過程中,可以將部分 Object 物件進行移動以降低記憶體碎片,因此根據記憶體物件是否支援移動分為可移動物件、不可移動物件;按照連續性分為連續性空間(ContinuousMemMapAllocSpace)和非連續空間(DiscontinuousSpace);最終每個子類的 Space 都繼承至 Space 和 ContinuousSpace/DisContinuousSpace,從上而下的繼承關係,如下圖:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

這些記憶體空間的例項化物件儲存在虛擬機器 Heap 物件中,申請記憶體時根據記憶體屬性選擇不同的記憶體空間進行分配和管理。

針對此文,我們重點關注的是大記憶體管理,以 LargeObjectMapSpace 為例,從上圖可以看到它繼承至非連續記憶體空間,對於非連續記憶體的管理要簡單得多,簡單總結如下:虛擬機器預設把大於 3 個物理頁(12K)的原子性物件或 String 型別的物件透過該空間進行管理,該空間所有的物件都儲存在一個 Map 容器中,每次 GC 時遍歷這些物件是否被引用,如果沒有被引用則直接釋放即可。由於這些大物件之間是離散的,因此不會造成記憶體碎片問題,在 GC 時也就無需進行複製壓縮以釋放連續空間。下面我們再簡單介紹一下 LargeObjectSpace 的角色和工作方式。

3.2 大物件記憶體管理

Java 大物件的記憶體管理比較簡單,主要集中在 LargeObjectSpace 模組,主要負責 Java 大物件的記憶體申請和釋放。

3.2.1 記憶體申請

結合原始碼,接下來簡單介紹一下 Java 大物件申請流程,整理流程圖如下。在此過程中,重點關注 Heap 已申請記憶體大小的更新過程和記憶體不足時丟擲 OOM 的過程

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

在上圖我們可以看到,在記憶體實際申請過程,會首先檢測當前物件是否滿足大物件,具體條件是:申請物件型別必須是原子型別或者 String 型別,並且要大於 3 個物理頁

inline bool Heap::ShouldAllocLargeObject(ObjPtr<mirror::Class> c, size_t byte_count) const {

  return byte_count >= large_object_threshold_ && (c->IsPrimitiveArray() || c->IsStringClass());

}

如果滿足以上條件,則會執行大物件申請流程,從 LargeObjectMapSpace 進行申請。但是在正式申請之前,會再次判斷是否記憶體觸頂,計算規則是將 Heap 中已經申請的記憶體和將要申請的記憶體進行相加,判斷是否超過虛擬機器的記憶體上限(growth_limit_),如果沒有超過則說明不會觸頂,然後就會直接從 LargeObjectMapSpace 申請一塊記憶體;如果大於 growth_limit_,則說明可能會觸頂,此時要先進行 GC,爭取釋放一些記憶體,如果多輪 GC 之後仍然滿足不了,則丟擲 OOM

在透過以上檢測之後,將會透過LargeObjectMapSpace::Alloc 例項化一個 Object 物件,如下圖:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

結合上圖,可以看到 LargeObjectMapSpace 在記憶體申請過程中如要完成以下工作:

  1. 根據申請的記憶體大小,利用 mem_map 對映與之對應的一塊記憶體;
  2. 將對映的記憶體轉換為 mirror::Object 物件;
  3. 將該 Object 物件與 mem_map 例項進行關聯,並存儲到 large_objects 集合;
  4. 更新 largeObjectMapSpace 當前記憶體佔用和物件數量,以及累計佔用大小和物件數量;

3.2.2 記憶體釋放

記憶體釋放邏輯:根據傳入的物件,先檢查是否在 large_objects_集合中,如果不存在則丟擲異常;否則同步更新(釋放)當前記憶體狀態,並從該集合移除該物件。

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

在介紹完虛擬機器記憶體空間管理以及 Large Object Space 記憶體管理方式的相關背景知識後,接下來我們迴歸到正題,看看我們針對虛擬機器是如何改造 Large Object Space 的記憶體管理策略的。

4. mSponge 實現原理

4.1 方案簡介

為了便於更好地理解,我們將整個方案分為 2 個部分進行介紹。

一期方案:主要介紹在 Java 大物件透過 LargeObjectSpace 的記憶體申請和釋放過程中,如何在記憶體申請和釋放過程對其進行改造,以脫離虛擬機器對這些物件的記憶體管理,最後實現 LargeObjectSpace 佔用的記憶體完全脫離虛擬機器記憶體統計。

二期方案:針對一期方案需要在應用執行過程中提前開啟,但是線上 99%以上執行過程中可能不會發生 OOM,因此一期方案對系統的侵入有點高。為了最佳化這個現象,二期方案主要透過監聽應用是否發生 OOM,如監測到 OOM 則攔截並同開啟一期方案“釋放更多可用記憶體”,然後重試記憶體申請,以挽救本次 OOM;如果沒有發生 OOM,則說明記憶體狀態良好,該方案就不需要開啟。顯然,這種智慧式的開啟方式和最大化的記憶體保障效果將是更加極致的解決方案。

4.2 命名由來

在執行過程中,該方案會隨著 LargeObjectSpace 的使用情況動態“吸收”和“釋放”虛擬機器 Heap 統計記憶體——“吸收”不希望被虛擬機器統計的 LargeObjectSpace 的記憶體,“釋放”已經透過 GC 回收的 Large Object 記憶體。整個執行過程猶如海綿吸水,故將該方案命名為:Memory Sponge,寓意:記憶體海綿,簡稱mSponge

4.3 mSponge 一期

從上面的大記憶體申請流程圖中可以看到,如果當前記憶體申請滿足 Java 大物件的條件(大於 12K),並在記憶體申請過程檢測是否記憶體觸頂時,“一直”返回 False,則可以透過 LargeObjectMapSpace 直接申請並返回物件例項,則可以繞過這裡的記憶體觸頂 OOM 問題。

同時,我們知道LargeObjectMapSpace 內部管理的物件是離散的,不支援虛擬機器 GC 過程中連續記憶體空間的複製壓縮的特性,因此即使是該空間記憶體佔用過多,導致總記憶體超過了上限(512M?),但是其它連續記憶體 Space 的記憶體閾值仍然保持正常範圍,因此不會影響到其他記憶體空間 GC 同構複製壓縮能力,也就不會破壞虛擬機器的記憶體管理。

4.3.1 方案思路

那麼如何才能在大物件記憶體觸頂檢測過程中繞開現有的檢測機制呢?透過調研發現判斷記憶體觸頂的關鍵條件在於虛擬機器中管理當前已申請記憶體 Heap::num_bytes_allocated_物件,即每次記憶體申請成功和 GC 釋放時,都會同步更新該值:

inline mirror::Object* Heap::AllocObjectWithAllocator(Thread* self,
ObjPtr<mirror::Class> klass, size*t byte_count, AllocatorType allocator, const PreFenceVisitor& pre_fence_visitor) {
......
 //實際申請記憶體過程
......
 if (bytes_tl_bulk_allocated > 0) {
size_t num_bytes_allocated_before =
//成功申請之後,需要同步更新虛擬機器整體 Heap 記憶體使用
num_bytes_allocated* . fetch_add ( bytes_tl_bulk_allocated , std :: memory_order_relaxed );
......
}
}

GC 過程中,當每個 Space 釋放一定物件和記憶體之後,會進一步同步到虛擬機器的 Heap 物件,同步更新虛擬機器整體記憶體使用,介面如下:

void Heap::RecordFree(uint64_t freed_objects, int64_t freed_bytes) {
  ......
  // Note: This relies on 2s complement for handling negative freed_bytes.
  //釋放之後,需要同步更新虛擬機器整體Heap記憶體使用
  num_bytes_allocated_  . fetch_sub (static_cast< ssize_t >( freed_bytes ), std :: memory_order_relaxed );
  ......
}

透過上面這個介面我們可以看到,每個 Space 在記憶體回收後都會更新虛擬機器更新整體記憶體使用情況,那麼我們是不是可以在合適的時機人為主動呼叫該介面,減去 LargeObjectMapSpace 管理的記憶體值,那麼 Heap::num_bytes_allocated_統計的就全部是其他記憶體 Space 的記憶體使用了;換而言之透過 LargeObjectMapSpace 申請的記憶體將會“脫離虛擬機器”Heap::num_bytes_allocated_的統計,納入 Native 層的記憶體管理。但是這些物件的引用和記憶體回收機制仍然由虛擬機器管理,因此並不會存在記憶體洩漏的隱患。

4.3.2 流程示意

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

LargeObjectMapSpace申請的記憶體,直接透過 Map 對映到虛擬記憶體,因此對於 32 位環境應用空間可對映記憶體在 3G 左右,但虛擬機器本身會搶先佔用 1G+的地址空間用於管理 Java 記憶體,因此應用側實際使用範圍在 2G 左右,極端情況下調整後的虛擬機器記憶體理論範圍將在 512M~2.5G,至於下限為何是 512M?理論上如果發生 OOM 時虛擬機器沒有任何大物件,這種情況下,則虛擬機器可用記憶體範圍將保持不變,因為我們改變的 Java 大物件的記憶體可用空間;示意圖如下:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

4.3.3 關鍵實現

上面介紹了該方案的背景知識和實現思路,接下來就要從技術層面考慮如何去實現了。如果在系統層面,直接從原始碼層面定製,相關改動會輕鬆很多,但是對應用側來說,要想相容不同 Android 版本,只有一條路可走——透過 InlineHook 代理相關介面,在執行過程中魔改相關引數以達到目的。在解決完介面代理問題之後,接下來還有下面幾件事情要解決:

  • 虛擬機器並沒有對外暴露獲取 LargeObjectMapSpace 記憶體的介面,如何才能實時獲取當前 Space 已申請的記憶體大小?
  • 如何在合適的時機同步 Heap::num_bytes_allocated_記憶體統計,以便於讓 LargeObjectMapSpace 的記憶體"脫離"虛擬機器的統計?
  • 如何"跳過"虛擬機器在記憶體釋放過程對記憶體大小一致性校驗的問題?

4.3.3.1 獲取 LargeObjectMapSpace 當前記憶體

針對第一個問題,儘管 LargeObjectSpace 中提供了獲取當前記憶體大小的介面(LargeObjectSpace::GetBytesAllocated),但是這個介面並沒有對外暴露,因此需要透過解析 Libart.so 中的"GetBytesAllocated"符號,以 Android Q 為例,該函式簽名符號為:_ZN3art2gc5space16LargeObjectSpace17GetBytesAllocatedEv;並在執行過程中動態獲取該符號在記憶體中的地址。

由於 GetBytesAllocated 是非靜態函式,因此在實際呼叫該介面時,需要知道當前物件的例項化物件,然後透過例項化物件呼叫該介面即可,在這裡,我們透過 inlineHook 代理"LargeObjectMapSpace::Alloc"獲取 LargeObjectMapSpace 的例項,LargeObjectMapSpace::Alloc 介面部分原始碼如下:

mirror::Object* LargeObjectMapSpace::Alloc(Thread* self, size_t num_bytes, size_t* bytes_allocated, size_t* usable_size, size_t* bytes_tl_bulk_allocated) {

  std::string error_msg;

  MemMap mem_map = MemMap::MapAnonymous("large object space allocation",
                                        num_bytes,
                                        PROT_READ | PROT_WRITE,
                                        /*low_4gb=*/ true,
                                        &error_msg);
  ......
  //申請成功後將當前記憶體佔用+ allocation_size
  num_bytes_allocated_  += allocation_size ;
  total_bytes_allocated_ += allocation_size;
  ++ num_objects_allocated_  ;  //申請成功後將當前記憶體數量+1
  ++total_objects_allocated_;
  return obj;
}

在獲取 LargeObjectMapSpace 的例項化物件之後,再透過該物件直接呼叫 GetBytesAllocated即可實時獲取當前 LargeObjectMapSpace 的記憶體大小

4.3.3.2"移除"LargeObjectSpace 記憶體

當我們可以實時獲取 LargeObjectSpace 的記憶體使用之後,接下來便是如何從虛擬機器 Heap 中“移除”LargeObjectSpace 實際佔用的記憶體了,透過調研發現可以透過解析"Heap::RecordFree"函式符號,並呼叫Heap::RecordFree 的函式介面,增加或減少 Heap 中我們想要更新的記憶體大小

void Heap::RecordFree(uint64_t freed_objects, int64_t freed_bytes) {
  ......
  // Note: This relies on 2s complement for handling negative freed_bytes.
  num_bytes_allocated_.fetch_sub (static_cast< ssize_t >( freed_bytes ), std :: memory_order_relaxed );
  ......
}

當上述條件滿足我們可以靈活更新虛擬機器 Heap 記憶體之後,接下來要處理的就是選擇一個合適的時機直接“移除”LargeObjectSpace 的記憶體,並且需要更新記錄當前 Heap“移除”的記憶體大小,經過調研並考慮到及時性和精準性,最終選擇了在 LargeObjectSpace 的記憶體申請和回收過程,對虛擬機器 Heap 記憶體進行動強制更新,以移除虛擬機器對 LargeObjectSpace 的記憶體統計。

在 Large Object 申請過程,如果記憶體申請成功,則在該 Object 例項化物件返回之前,先強制從虛擬機器記憶體統計中減去該部分記憶體,接下來虛擬機器內部會在返回例項化物件之後,並統計本次新增記憶體,在這裡我們透過先減後加的方式,維持了整個記憶體水位不變,從而間接地實現了虛擬機器“忽略”了本次記憶體開銷。

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

如果後續 GC 過程中釋放了 LargeObjectSpace 中的部分或者全部物件,正常情況下釋放的記憶體會同步同步到 Heap,以便於更新整體使用記憶體及可用記憶體,但是從上面的分析中我們知道,其實 LargeObjectSpace 的記憶體已經不在 Heap 統計之中了,如果從 Heap 中減去釋放的這些記憶體,那麼將會導致 Heap 統計的記憶體偏少,因此需要主動將該部分釋放的記憶體"補償"回來,避免統計錯亂。

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

透過上述步驟實現了在記憶體回收過程中對大物件記憶體管理的改造,改造之後 Heap 統計的記憶體將不再包含 LargeObjectSpace 管理的記憶體,從而間接地擴大了其他記憶體 Space 使用上限;對於 LargeObjectSpace 來說,虛擬機器統計到的該記憶體 Space 一直為 0,但是 LargeObjectSpace 內部並沒有記憶體限制,異常該 Space 的記憶體使用上限將會顯著提升,針對 Android O 以下系統來說,這部分記憶體 Space 不僅包含 Bitmap,還包含其他大物件。

4.3.3.3 記憶體校驗

在適配過程中,發現 Android L 版本之後,虛擬機器會在 GC 過程中對釋放記憶體和使用記憶體進行一次校驗。如果發現當前使用記憶體加上釋放記憶體小於 GC 之前的記憶體,則會丟擲“斷言”異常,相關原始碼如下:

void Heap::GrowForUtilization(collector::GarbageCollector* collector_ran,

                              uint64_t bytes_allocated_before_gc) {

   //GC結束後,再次獲取當前虛擬機器記憶體大小

  const uint64_t bytes_allocated = GetBytesAllocated();
  ......
 if (!ignore_max_footprint_) {
        const uint64_t freed_bytes = current_gc_iteration_.GetFreedBytes() +
          current_gc_iteration_.GetFreedLargeObjectBytes() +
          current_gc_iteration_.GetFreedRevokeBytes();
    //GC之後虛擬機器已使用記憶體加上本次GC釋放記憶體理論上要大於等於GC之前虛擬機器使用的記憶體,如果不滿足,則丟擲Fatel異常!!!
  CHECK_GE ( bytes_allocated + freed_bytes , bytes_allocated_before_gc );
 }
  ......

}

因為我們在記憶體 GC 過程中,動態調整了 Heap 當前使用記憶體大小,這可能會導致 gc 結束後再次獲取的 Heap 當前使用記憶體小於實際值,為了不影響校驗邏輯,需要代理 Heap::GrowForUtilization 介面,強制將bytes_allocated_before_gc 引數設定為 0,以保證校驗恆成立(針對該處調整從後續邏輯和實際測試來看,對後續記憶體 GC 並無明顯影響)

4.3.4 小結

至此,透過上述思路和技術方案完成了 Android 虛擬機器記憶體統計策略的改造,該方案不僅間接提升了虛擬機器其它記憶體空間執行時的使用上限,也將 LargeObjectSpace 的記憶體使用上限完全脫離了虛擬機器的限制,完全等同於 Native 記憶體屬性進行管理。因此該方案相比 Android 系統 Bitmap 記憶體管理改造更加徹底,給應用的記憶體環境帶來了極大的改善。

4.4 mSponge 方案二期

在上文,對記憶體統計策略改造之後可以很大程度釋放 LargeObjectSpace 記憶體空間以最佳化 Java OOM 問題,但是進一步思考之後,發現一期方案並不是最優解,因為應用執行過程中很大機率不會發生 OOM,如果能監聽到 OOM 時,再啟動最佳化方案,同時再救活本次 OOM,那麼這種智慧化的按需開啟將是極致化的解決方案

4.4.1 方案思路

針對上述思考,基於 mSponge 方案一期的設計,決定採用“OOM 探測+按需開啟”的策略來完成對記憶體的按需擴充套件。即:對記憶體申請過程進行定向監控,當監聽到記憶體不足即將丟擲 OOM 異常時,進行攔截,並激活 mSponge 方案一期記憶體最佳化方案(從 Heap 記憶體統計中移除當前 LargeObjectSpace 使用記憶體);然後再觸發一次記憶體申請,以保證記憶體成功申請。按照上述思路,整理方案最佳化前後對比示意圖:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

4.4.2 流程示意

基於上面的思路,我們需要在虛擬機器內部監聽並攔截 OOM,當監聽到第一次 OOM 時主動將 LargeObjectSpace 的記憶體從 Heap 統計中移除,以增加空閒記憶體,同時再開啟 mSponge 一期最佳化策略,以保證後續 LargeObjectSpace 的記憶體變化不會影響 Heap 記憶體統計;按照這種思路,整體二期方案示意圖如下:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

4.4.3 關鍵實現

二期技術實現主要涉及下面幾個流程:監聽並判斷是否需要攔截 OOM 異常;監聽記憶體分配結果;重試記憶體申請。具體如下:

  • 監聽 OOM 異常:代理 Heap::ThrowOutOfMemoryError,監聽記憶體分配失敗時丟擲 OOM 的過程,並判斷是否需要攔截,如果可攔截。
  • 監聽記憶體分配結果:代理 Heap::AllocateInternalWithGc ,監聽本次記憶體申請過程中,是否發生了 OOM 並被攔截,如果發生 OOM 並被攔截,則再次觸發記憶體申請,以保證記憶體申請成功。
  • 重試記憶體申請:透過 AllocateInternalWithGc 再次觸發一次記憶體申請,並在此之前禁止攔截本次記憶體申請過程中可能丟擲的 OOM,如果成功申請,則返回該物件。

4.4.3.1 監聽 ThrowOutOfMemoryError

透過 inlineHook 代理"Heap::ThrowOutOfMemoryError"並監聽該介面,如果該介面被呼叫則說明,當前記憶體不足或者沒有連續記憶體,無法滿足本次記憶體需求;並根據呼叫 AllocateInternalWithGc 代理介面設定的標識"sAllowsSkipThrowOutOfMemoryError",判斷是否攔截本次 OOM 異常;實現如下:

void ThrowOutOfMemoryErrorProxy(void* heap, void* self, size_t byte_count, AllocatorType allocator_type){
    if(isAllowWork()) {
        sFindThrowOutOfMemoryError = true;
    if (sAllowsSkipThrowOutOfMemoryError
 && !sForceAllocateInternalWithGc) {
             //攔截並跳過本次OutOfMemory,並置標記位
            sSkipThrowOutOfMemoryError = true;

            //TODO:將LargeObjectSpace記憶體從Heap中移除
            return;
        }
        sSkipThrowOutOfMemoryError = false;

         //如果不允許攔截,則直接呼叫原函式,丟擲OOM異常
        ThrowOutOfMemoryErrorOrigin(heap, self, byte_count, allocator_type);
    } else{
        ThrowOutOfMemoryErrorOrigin(heap, self, byte_count, allocator_type);
    }
}

4.4.3.2 代理 AllocateInternalWithGc

透過 inlineHook 代理"Heap::AllocateInternalWithGc"代理並監聽該介面,因為在該介面執行過程中會觸發一次或多次 GC,如果依然滿足不了本次記憶體申請,則會丟擲 OOM 並返回 NULL;因此可以透過代理該介面可以知道本次記憶體申請是否成功,以及本次申請過程中是否丟擲 OOM 異常;如果返回物件為 NULL,並攔截了 OOM 異常,則設定禁止攔截 OOM 標記之後,呼叫 Heap::AllocateInternalWithGc 原介面再次進行記憶體申請,以保證成功申請記憶體。

原則上透過 mSpnge 方案將 LargeObjectSpace 的記憶體從 Heap 移除之後,理論上虛擬機器可用記憶體會增加很多,基本能保證本次記憶體成功申請(極端情況仍會出現記憶體不足,正常丟擲 OOM 即可)。

void* AllocateInternalWithGcProxy(void* heap, void* thread,
                                  AllocatorType allocator,
                                  bool instrumented,
                                  size_t alloc_size,
                                  size_t* bytes_allocated,
                                  size_t* usable_size,
                                  size_t* bytes_tl_bulk_allocated,
                                  void* klass){
    if(isAllowWork()) {
         //設定標記位,允許攔截本次記憶體申請過程的OOM
        sAllocateInternalWithGc = true;

        sForceAllocateInternalWithGc = false;

        //呼叫原始介面,並判斷返回object是否未NULL
        void *object = AllocateInternalWithGcOrigin(heap, thread, allocator, instrumented, alloc_size, bytes_allocated, usable_size, bytes_tl_bulk_allocated, klass);
        sAllocateInternalWithGc = false;
          //如果返回object為NULl,並且在此過程中丟擲了OOM異常,則說明記憶體不足導致申請失敗,則開啟虛擬機器記憶體統計策略最佳化方案,釋放LargeObjectSpace記憶體
        if (object == NULL && sAllowsSkipThrowOutOfMemoryError && sSkipThrowOutOfMemoryError) {

             //設定標記位,不允許攔截本次記憶體申請過程的OOM
            sForceAllocateInternalWithGc = true;

             //再次呼叫記憶體申請,爭取成功申請記憶體
            object = AllocateInternalWithGcOrigin(heap, thread, allocator, instrumented, alloc_size, bytes_allocated, usable_size, bytes_tl_bulk_allocated, klass);

            sForceAllocateInternalWithGc = false;
        }
        return object;
    } else{

        return AllocateInternalWithGcOrigin(heap, thread, allocator, instrumented, alloc_size,
                                     bytes_allocated, usable_size,
                                     bytes_tl_bulk_allocated, klass);
    }
}

4.4.4 小結

至此,透過上述思路和技術方案完成了應用記憶體申請過程中的 OOM 監測,並在虛擬機器記憶體丟擲 OOM 的過程對 Heap 的記憶體管理進行了改造,移除了 LargeObjectSpace 的記憶體佔用,間接增加了虛擬機器可用記憶體之後,再次觸發記憶體申請,以拯救本次 OOM;透過這種按需開啟的方式,體現了以最小化的侵入成本換來最大化的記憶體保障。

5.方案收益

以今日頭條 32 位測試環境為例,透過該方案對虛擬機器大物件記憶體擴充套件了 500M(實際使用可根據產品自身特點設定擴充套件記憶體大小),透過測試用例連續佔用 500M 記憶體後,虛擬機器記憶體分佈如下圖:

拯救OOM!位元組自研 Android 虛擬機器記憶體管理最佳化黑科技 mSponge

從上圖可以清晰看到虛擬機器其它記憶體空間(main space)仍然是 1G 大小,但是large space 記憶體大小接近 600M,記憶體可用範圍得到明顯增加

5.1 統一 Large Object 管理策略

透過該最佳化方案,實現了在應用側對 Android 各版本虛擬機器記憶體管理策略的統一,彌補了 Android 系統最佳化向下相容性不足的缺陷,很大程度降低了產品設計和 RD 開發過程,需要對低版本記憶體管理差異性進行降級或相容的成本。


系統版本


虛擬機器最大可用記憶體


LargeObjectSpace 最大可用記憶體


說明


Android O 以下


512M


512M


1.所有記憶體空間共享虛擬機器 512M 記憶體

2.Bitmap 源資料納入 Java 記憶體統計

3.LargeObjectSpace 與其它記憶體空間共享


Android O 以上


512M


512M


1.所有記憶體空間共享虛擬機器 512M 內

2.Bitmap 源資料納入 Java 記憶體統計

3.LargeObjectSpace 與其它記憶體空間共享


mSponge 改造方案


512M


32 位:2G/3G


1.除 LargeObjectSpace 之外,其它記憶體空間共享 512M 記憶體

2.LargeObjectSpace 整體記憶體不再納入 Java 記憶體統計

5.2 OOM 收益

統計近半年頭條線上 OOM Case(集中發生在 Android O 以下版本,因為低版本 Bitmap 記憶體納入虛擬機器管理和統計,更加容易導致記憶體觸頂),都發生在部分圖片快取過高,或者各種節日運營導致 Bitmap 快取不合理;由於該方案將虛擬機器大記憶體的統計進行了最佳化,相當於“擴充套件”了虛擬機器可用記憶體,因此對於上述場景導致的 Java OOM 問題,起到很好的容錯能力,挽救 90%以上的 OOM 問題。

6.總結

透過上文,我們以 32 位執行環境為例,介紹了Android 虛擬機器記憶體管理策略的改造思路,改造之後的記憶體管理策略,統一了 Android 系統碎片化的記憶體管理差異,進一步最佳化應用的執行環境,顯然更加符合應用側對 Java 記憶體的使用訴求,更好地應對和保障了應用執行時的穩定性,帶給使用者更好體驗。

7.後續

在移動網際網路快速迭代的背景下,各類應用也在快速迭代,如何更好地保障線上質量,將是一種長期需要應對和探索的方向,除了在常規視角進行最佳化之外,如何進行更深層次的系統探索,也是我們日常工作的主要方向,後續我們將會分享更多關於系統層面的相關實踐。

加入我們

我們是位元組跳動 Android 平臺架構團隊,以服務今日頭條為主,面向 GIP,同時服務公司其他產品,在產品效能穩定性等使用者體驗、研發流程、架構方向上持續最佳化和探索,滿足產品快速迭代的同時,保持較高的使用者體驗。

如果你對技術充滿熱情,想要迎接更大的挑戰和舞臺,歡迎加入我們。北京、深圳均有崗位,感興趣傳送郵箱:[email protected] ,郵件標題:姓名 - GIP - Android 平臺架構

分類: 娛樂
時間: 2022-01-14

相關文章

煮雞蛋時,不能只用清水!多加這2步,煮出的雞蛋又香又嫩好剝殼

煮雞蛋時,不能只用清水!多加這2步,煮出的雞蛋又香又嫩好剝殼
文/小花談美食 在我們的生活中,很多人都喜歡吃雞蛋,因為雞蛋不僅營養豐富,有利於人的身體健康,而且它的做法也很多,用雞蛋為食材做出的雞蛋餅.糕點.湯羹等,都深受大家的喜愛,老人小孩都愛吃.雞蛋最簡單的 ...

怕上火,用“煮菜水”給新生兒衝奶,嬰兒嚴重亞硝酸鹽中毒

怕上火,用“煮菜水”給新生兒衝奶,嬰兒嚴重亞硝酸鹽中毒
又是被亂來的家長氣哭的一天. 前陣子刷微博,看到這樣一條新聞. 家長覺得新生兒喝奶粉會"上火",自行用熬煮的蔬菜水為寶寶衝奶,煮一頓,喝一天,然後蔬菜中的硝酸鹽類物質在微生物的作用 ...

《劇場版咒術回戰0》公開五條悟單眼造型,粉絲質疑無視原作
預定2021年12月24日,日本上映的<劇場版咒術回戰0>近日公佈了作品中數一數二的強者五條悟的造型,但有關造型一公佈,就令到不少觀眾不接受. <劇場版咒術回戰0>改編自漫畫家 ...

雞蛋別再用水煮了,保姆教你新方法,只需1分鐘,雞蛋鮮嫩不粘殼

雞蛋別再用水煮了,保姆教你新方法,只需1分鐘,雞蛋鮮嫩不粘殼
大家好,這裡是每天分享一個生活美食小妙招的金芒美食.在日常生活,我們都會煮一些水煮雞蛋給孩子老人吃,因為雞蛋的營養成分是比較高的.那如何煮雞蛋不僅不破殼,而且也不需要我們專門去看一下水是否幹了呢?今天 ...

隔夜菜、隔夜水和隔夜雞蛋,哪個不能吃?你吃錯了幾個?

隔夜菜、隔夜水和隔夜雞蛋,哪個不能吃?你吃錯了幾個?
隔夜菜因為不是新鮮的食物,所以被認為是不健康的食品.但是人們對於隔夜菜的定義卻有所爭論,普通人認為當天沒有吃完的剩下,放置在冰箱裡不動,直到第二天的食物,叫做剩菜,但是嚴格來說,若食物被已經煮熟後,放 ...

隔夜肉、隔夜菜、隔夜水和隔夜雞蛋,哪個不能吃?你吃錯了嗎?

隔夜肉、隔夜菜、隔夜水和隔夜雞蛋,哪個不能吃?你吃錯了嗎?
一提到隔夜菜,相信大家都並不陌生,也都對不能吃隔夜菜這一觀念有著很深的認同.隨著人們生活水平的不斷提高,除了對日常生活飲食的需求有了基本的滿足之外,還越發強調飲食的高品質.大健康理念亦在不斷地深入人心 ...

煮水煮蛋,教你一招不用鍋不用電,效果真厲害,天天吃新鮮的蛋

煮水煮蛋,教你一招不用鍋不用電,效果真厲害,天天吃新鮮的蛋
哈嘍,大家好,我是捲毛.在文章開始前,捲毛想先問問各位上班時,一般會選擇什麼型別的早餐呢?是雞蛋包子?還是牛奶蛋糕?我估計在南方的朋友們,你們是不是會吃米粉比較多.別問我怎麼那麼瞭解,捲毛可是個地地道 ...

BBC起底健康飲食真相:油煎雞蛋更健康,水煮西藍花最不營養?

BBC起底健康飲食真相:油煎雞蛋更健康,水煮西藍花最不營養?
不管是減肥瘦身,還是美容美膚,肯定都繞不開"飲食"這座大山. 所以,很多人為了達到自己瘦身.健康等目的,在"吃"上面可謂是用心良苦,比如吃西藍花必須水煮,吃雞蛋 ...

印度耆那教:最純正的“素食者”,認為吃土豆也算“殺生”

印度耆那教:最純正的“素食者”,認為吃土豆也算“殺生”
早前在國外工作的時候,有一次學校來了位印度專家,因為早就瞭解到了此人是素食者,校長秘書特意給定了純素的"齋飯". 沒成想,進餐的時候,竟然還是鬧出了一些尷尬.原來,素食中,有炸土豆 ...

雞蛋的10道好吃做法,每天換著做,孩子想挑食都難,頓頓都吃光碟

雞蛋的10道好吃做法,每天換著做,孩子想挑食都難,頓頓都吃光碟
今天蓉兒為大家分享雞蛋的10道好吃做法,花樣很多,有捲餅有炒菜有甜品還有糕點,對於特別挑食不愛吃雞蛋的小朋友和大朋友們,一定可以俘獲她(他)的胃. ┄[菠菜雞蛋卷]┄ [ 食材清單 ]菠菜.雞蛋.麵粉 ...

減肥無需節食,分享七款減脂菜,低脂又美味,多吃不胖

減肥無需節食,分享七款減脂菜,低脂又美味,多吃不胖
減肥無需節食,分享七款減脂菜,低脂又美味,多吃不胖! 大家好!我是紫色浪漫,又到了分享美食的時間.最近有點忙,有好多天沒有更新美食了.不過再忙,減肥的事情不能中斷,每天稱體重,每天給自己做減脂餐,今天 ...

雞蛋的8種“高階”吃法,不知道吃啥收藏好,簡單解饞很下飯

雞蛋的8種“高階”吃法,不知道吃啥收藏好,簡單解饞很下飯
恐怕大多數人的學廚之道都是從一盤炒雞蛋開始的,雞蛋在日常烹飪中變幻萬千,能炒.能煎.能煮.能蒸,還能做成各種糕點,算得上是優質不可或缺的食材之一.雞蛋物美價廉,簡單易得,卻又難得的鮮香味美,不知道吃啥 ...

中秋家宴將至,花200元做10道菜!紅紅火火,上桌全家都誇上檔次

中秋家宴將至,花200元做10道菜!紅紅火火,上桌全家都誇上檔次
中秋節就要到了,你的家宴選單準備好了嗎?今兒蓉兒準備的10道好菜,每一道都顏值超高,紅紅火火的,家宴待客也特別有面兒,下面一起來看看吧! ┄[白菜燒麥]┄ 這是一道用白菜做的燒麥,顏值高味道好. [ ...

中秋家宴,推薦給你5道硬菜做法,待客倍有面子,實惠又營養

中秋家宴,推薦給你5道硬菜做法,待客倍有面子,實惠又營養
中秋家宴,推薦給你6道硬菜做法,待客倍有面,實惠有營養,親愛的好朋友們,大家好,我是大廚江一舟,今天又到了,給大家分享美食的時刻了,你們準備好了嗎? 馬上到過中秋節了,一桌美味的團圓飯,是少不了了,很 ...

家有倆娃每餐都要準時做,四菜一湯安排好,孩子說要捏著鼻子吃飯

家有倆娃每餐都要準時做,四菜一湯安排好,孩子說要捏著鼻子吃飯
暑假還沒結束,媽媽還需繼續堅持,還有不到一週就能開學了,有多少全職媽媽在期待啊!這一個假期,媽媽們都沒閒著! 比如我家,家有倆娃,每餐都要準時做,還要有葷有素有湯喝,平時做飯都遷就她倆的口味,比較清淡 ...

5種最吸油的食材,雞蛋也上榜了!味道雖好但油膩,難怪越吃越胖

5種最吸油的食材,雞蛋也上榜了!味道雖好但油膩,難怪越吃越胖
日常烹飪菜餚,用油那是必不可少的,炒菜用油,能讓食物味道更好地混合,並且能保持食物色澤,煎炸用油,能讓食物透過煎炸方式,改變食物口感,達到外皮酥脆.內裡滑嫩的層次口感,而且讓這股香酥的味道充分散發,誘 ...

過去土豆僅雞蛋大,為何現在的土豆越來越大?3點原因有關

過去土豆僅雞蛋大,為何現在的土豆越來越大?3點原因有關
前言 導讀:過去土豆僅雞蛋大,現在的土豆比拳頭還要大,是用了膨大劑嗎? 土豆是一種舶來品,它原產自南美洲安第斯山區.目前也是全球第四大糧食作物,其種植面積僅次於水稻.玉米和小麥. 因為土豆是從國外傳來 ...

包菜最好的做法,簡單家常,爽口又下飯,不吃撐不停嘴,香啊

包菜最好的做法,簡單家常,爽口又下飯,不吃撐不停嘴,香啊
提到做包菜,大部分人只想到香辣的手撕包菜,卻不知道包菜還有別的吃法. 今天做的是一道簡單的包菜胡蘿蔔餅!包菜和胡蘿蔔都是普通的家常菜,用包菜和胡蘿蔔做餅,營養美味又簡單.做出來的餅咬一口,滿嘴都是焦脆 ...

秋天,雞蛋和它是最佳搭檔,營養足特解饞,比大魚大肉吃著舒服

秋天,雞蛋和它是最佳搭檔,營養足特解饞,比大魚大肉吃著舒服
秋季非常燥熱,雖然沒有夏季悶熱天氣,晚上也變得涼爽一些,白天還特別炎熱,總是提不起胃口來,平常總是為吃啥而操心,前段時間天天吃肉,這幾天準備換一些新鮮菜餚,比方說這盤尖椒蟹味菇炒雞蛋,這菜讓人大飽口福 ...

萬能滷水配方大公開,想吃啥就滷啥,連骨頭都入味,3斤都不夠吃

萬能滷水配方大公開,想吃啥就滷啥,連骨頭都入味,3斤都不夠吃
說起滷味,相信你的腦海裡就會出現許多鴨脖.鴨翅.雞爪.豬蹄.滷蛋等,想想都流口水.很多人都曾想過自己在家滷,但無奈不瞭解滷水的製作方法和配方,只能眼饞國內姐妹們的朋友圈- 其實,滷水有一個萬能配方,不 ...