ART視角 | 如何自動回收native記憶體

語言: CN / TW / HK

前言

GC用於Java堆記憶體的回收,這是人盡皆知的事實。然而現在有些Java類被設計成牽線木偶,Java物件只儲存一些“線”,其真實的記憶體消耗全都放到了native記憶體中。譬如Bitmap。對它們而言,如何自動回收操縱的native記憶體成為一個亟須解決的問題。

想要自動回收,必須依賴GC機制。但僅僅依靠現有的GC機制還不夠。我們還需要考慮以下兩點:

  1. 如何在native記憶體增長過多的時候 自動 觸發GC

  2. 如何在GC回收Java物件時 同步回收 native資源

Android從N開始引入了NativeAllocationRegistry類。早期的版本可以保證在GC回收Java物件時同步回收native資源(上述第2點),其內部用到的正是上一篇部落格介紹過的Cleaner機制。

利用早期版本的NativeAllocationRegistry,native資源雖然可以回收,但仍然有些缺陷。譬如被設計成牽線木偶的Java類所佔空間很小,但其間接引用的native資源佔用很大。因此就會導致Java堆的增長很慢,而native堆的增長很快。在某些場景下,Java堆的增長還沒有達到下一次GC觸發的水位,而native堆中的垃圾已經堆積成山。由程式主動呼叫 System.gc() 當然可以緩解這個問題,但開發者如何控制這個頻率?頻繁的話就會降低執行效能,稀疏的話就會導致native垃圾無法及時釋放。因此新版本的NativeAllocationRegistry連同GC一起做了調整,使得程序在native記憶體增長過多的時候可以自動觸發GC,也即上述的第1點。相當於以前的GC觸發只考慮Java堆的使用大小,現在連同native堆一起考慮進去了。

native垃圾堆積成山的問題會導致一些嚴重的問題,譬如最近國內很多32位APK上碰到過的native記憶體OOM問題,其中位元組跳動專門發過部落格介紹他們的解決方案。在連結的部落格裡,位元組跳動團隊提供了應用層的解決方案,由應用層來主動釋放native資源。但這個問題的根本解決還得依賴底層設計的修改。看完位元組跳動的部落格後,我專門聯絡過Android團隊,建議他們在CameraMetadataNative類中使用NativeAllocationRegistry。他們很快接受了這個提議,並提供了新的實現。相信位元組跳動遇到的這個問題在S上將不會存在。

目錄

1. 如何在native記憶體增長過多時自動觸發GC

當Java類被設計成牽線木偶時,其native記憶體的分配通常有兩種方式。一種是malloc(new的內部通常也是呼叫malloc)分配堆記憶體,另一種是mmap分配匿名頁。二者最大的區別是malloc通常用於小記憶體分配,而mmap通常用於大記憶體分配。

當我們使用NativeAllocationRegistry為該Java物件自動釋放native記憶體時,首先需要呼叫 registerNativeAllocation ,一方面告知GC本次native分配的資源大小,另一方面檢測是否達到GC的觸發條件。根據記憶體分配方式的不同,處理方式也不太一樣。

libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java

290     // Inform the garbage collector of the allocation. We do this differently for
291 // malloc-based allocations.
292 private static void registerNativeAllocation(long size) {
293 VMRuntime runtime = VMRuntime.getRuntime();
294 if ((size & IS_MALLOCED) != 0) { <==================如果native記憶體是通過malloc方式分配的,則走這個if分支
295 final long notifyImmediateThreshold = 300000;
296 if (size >= notifyImmediateThreshold) { <=========如果native記憶體大於等於300000bytes(~300KB),則走這個分支
297 runtime.notifyNativeAllocationsInternal();
298 } else { <==================如果native記憶體小於300000bytes,則走這個分支
299 runtime.notifyNativeAllocation();
300 }
301 } else {
302 runtime.registerNativeAllocation(size);
303 }
304 }


1.1 Malloc記憶體

Malloc分配的記憶體會有兩個判斷條件。

CheckGCForNative
CheckGCForNative

接下來看看 CheckGCForNative 函式內部的邏輯。

首先計算當前native記憶體的總大小,然後計算當前記憶體大小和閾值之間的比值,如果比值≥1,則請求一次新的GC。

art/runtime/gc/heap.cc

3927 inline void Heap::CheckGCForNative(Thread* self) {
3928 bool is_gc_concurrent = IsGcConcurrent();
3929 size_t current_native_bytes = GetNativeBytes(); <================獲取native記憶體的總大小
3930 float gc_urgency = NativeMemoryOverTarget(current_native_bytes, is_gc_concurrent); <============計算當前記憶體大小和閾值之間的比值,大於等於1則表明需要一次新的GC
3931 if (UNLIKELY(gc_urgency >= 1.0)) {
3932 if (is_gc_concurrent) {
3933 RequestConcurrentGC(self, kGcCauseForNativeAlloc, /*force_full=*/true); <=================請求一次新的GC
3934 if (gc_urgency > kStopForNativeFactor
3935 && current_native_bytes > stop_for_native_allocs_) {
3936 // We're in danger of running out of memory due to rampant native allocation.
3937 if (VLOG_IS_ON(heap) || VLOG_IS_ON(startup)) {
3938 LOG(INFO) << "Stopping for native allocation, urgency: " << gc_urgency;
3939 }
3940 WaitForGcToComplete(kGcCauseForNativeAlloc, self);
3941 }
3942 } else {
3943 CollectGarbageInternal(NonStickyGcType(), kGcCauseForNativeAlloc, false);
3944 }
3945 }
3946 }


獲取當前native記憶體的總大小需要呼叫 GetNativeBytes 函式。其內部統計也分為兩部分,一部分是通過 mallinfo 獲取的當前malloc的總大小。由於系統有專門的API獲取這個資訊,所以在 NativeAllocationRegistry.registerNativeAllocation 的時候不需要專門去儲存單次malloc的大小。另一部分是native_bytes_registered_欄位記錄的所有註冊過的mmap大小。二者相加,基本上反映了當前程序native記憶體的整體消耗。

art/runtime/gc/heap.cc

2533 size_t Heap::GetNativeBytes() {
2534 size_t malloc_bytes;
2535 #if defined(__BIONIC__) || defined(__GLIBC__)
2536 IF_GLIBC(size_t mmapped_bytes;)
2537 struct mallinfo mi = mallinfo();
2538 // In spite of the documentation, the jemalloc version of this call seems to do what we want,
2539 // and it is thread-safe.
2540 if (sizeof(size_t) > sizeof(mi.uordblks) && sizeof(size_t) > sizeof(mi.hblkhd)) {
2541 // Shouldn't happen, but glibc declares uordblks as int.
2542 // Avoiding sign extension gets us correct behavior for another 2 GB.
2543 malloc_bytes = (unsigned int)mi.uordblks;
2544 IF_GLIBC(mmapped_bytes = (unsigned int)mi.hblkhd;)
2545 } else {
2546 malloc_bytes = mi.uordblks;
2547 IF_GLIBC(mmapped_bytes = mi.hblkhd;)
2548 }
2549 // From the spec, it appeared mmapped_bytes <= malloc_bytes. Reality was sometimes
2550 // dramatically different. (b/119580449 was an early bug.) If so, we try to fudge it.
2551 // However, malloc implementations seem to interpret hblkhd differently, namely as
2552 // mapped blocks backing the entire heap (e.g. jemalloc) vs. large objects directly
2553 // allocated via mmap (e.g. glibc). Thus we now only do this for glibc, where it
2554 // previously helped, and which appears to use a reading of the spec compatible
2555 // with our adjustment.
2556 #if defined(__GLIBC__)
2557 if (mmapped_bytes > malloc_bytes) {
2558 malloc_bytes = mmapped_bytes;
2559 }
2560 #endif // GLIBC
2561 #else // Neither Bionic nor Glibc
2562 // We should hit this case only in contexts in which GC triggering is not critical. Effectively
2563 // disable GC triggering based on malloc().
2564 malloc_bytes = 1000;
2565 #endif
2566 return malloc_bytes + native_bytes_registered_.load(std::memory_order_relaxed);
2567 // An alternative would be to get RSS from /proc/self/statm. Empirically, that's no
2568 // more expensive, and it would allow us to count memory allocated by means other than malloc.
2569 // However it would change as pages are unmapped and remapped due to memory pressure, among
2570 // other things. It seems risky to trigger GCs as a result of such changes.
2571 }


得到當前程序native記憶體的總大小之後,便需要抉擇是否需要一次新的GC。

決策的過程如下,原始碼下面是詳細解釋。

art/runtime/gc/heap.cc

3897 // Return the ratio of the weighted native + java allocated bytes to its target value.
3898 // A return value > 1.0 means we should collect. Significantly larger values mean we're falling
3899 // behind.
3900 inline float Heap::NativeMemoryOverTarget(size_t current_native_bytes, bool is_gc_concurrent) {
3901 // Collection check for native allocation. Does not enforce Java heap bounds.
3902 // With adj_start_bytes defined below, effectively checks
3903 // <java bytes allocd> + c1*<old native allocd> + c2*<new native allocd) >= adj_start_bytes,
3904 // where c3 > 1, and currently c1 and c2 are 1 divided by the values defined above.
3905 size_t old_native_bytes = old_native_bytes_allocated_.load(std::memory_order_relaxed);
3906 if (old_native_bytes > current_native_bytes) {
3907 // Net decrease; skip the check, but update old value.
3908 // It's OK to lose an update if two stores race.
3909 old_native_bytes_allocated_.store(current_native_bytes, std::memory_order_relaxed);
3910 return 0.0;
3911 } else {
3912 size_t new_native_bytes = UnsignedDifference(current_native_bytes, old_native_bytes); <=======(1)
3913 size_t weighted_native_bytes = new_native_bytes / kNewNativeDiscountFactor <=======(2)
3914 + old_native_bytes / kOldNativeDiscountFactor;
3915 size_t add_bytes_allowed = static_cast<size_t>( <=======(3)
3916 NativeAllocationGcWatermark() * HeapGrowthMultiplier());
3917 size_t java_gc_start_bytes = is_gc_concurrent <=======(4)
3918 ? concurrent_start_bytes_
3919 : target_footprint_.load(std::memory_order_relaxed);
3920 size_t adj_start_bytes = UnsignedSum(java_gc_start_bytes, <=======(5)
3921 add_bytes_allowed / kNewNativeDiscountFactor);
3922 return static_cast<float>(GetBytesAllocated() + weighted_native_bytes) <=======(6)
3923 / static_cast<float>(adj_start_bytes);
3924 }
3925 }


首先將本次native記憶體總大小和上一次GC完成後的native記憶體總大小進行比較。如果小於上次的總大小,則表明native記憶體的使用水平降低了,因此完全沒有必要進行一次新的GC。

但如果這次native記憶體使用增長的話,則需要進一步計算當前值和閾值之間的比例關係,大於等於1的話就需要進行GC。下面詳細介紹原始碼中的(1)~(6)。

(1)計算本次native記憶體和上次之間的差值,這個差值反映了native記憶體中新增長部分的大小。

(2)給不同部分的native記憶體以不同的權重,新增長部分除以2,舊的部分除以65536。之所以給舊的部分權重如此之低,是因為native堆本身是沒有上限的。這套機制的初衷並不是限制native堆的大小,而只是防止兩次GC間native記憶體垃圾積累過多。

(3)所謂的閾值並不是為native記憶體單獨設立的,而是為(Java堆大小+native記憶體大小)整體設立的。add_bytes_allowed表示在原有Java堆閾值的基礎上,還可以允許的native記憶體大小。 NativeAllocationGcWatermark 根據Java堆閾值計算出允許的native記憶體大小,Java堆閾值越大,允許的值也越大。 HeapGrowthMultipiler 對於前臺應用是2,表明前臺應用的記憶體管控更鬆,GC觸發頻率更低。

(4)同等條件下,同步GC的觸發水位要低於非同步GC,原因是同步GC在垃圾回收時也會有新的物件分配,因此加上這些新分配的物件最好也不要超過閾值。

(5)將Java堆閾值和允許的native記憶體相加,作為新的閾值。

(6)將Java堆已分配的大小和調整權重後的native記憶體大小相加,並將相加後的結果除以閾值,得到一個比值來判定是否需要GC。

通過如下程式碼可知,當比值≥1時,將請求一次新的GC。

art/runtime/gc/heap.cc

3931   if (UNLIKELY(gc_urgency >= 1.0)) {
3932 if (is_gc_concurrent) {
3933 RequestConcurrentGC(self, kGcCauseForNativeAlloc, /*force_full=*/true); <=================請求一次新的GC


1.2 MMap記憶體

mmap的處理方式和malloc基本相當,大於300,000 bytes或mmap三百次都執行 CheckGCForNative 。唯一的區別在於mmap需要將每一次的大小都計入native_bytes_registered中,因為mallinfo中並不會記錄這個資訊(針對bionic庫而言)。

art/runtime/gc/heap.cc

3957 void Heap::RegisterNativeAllocation(JNIEnv* env, size_t bytes) {
3958 // Cautiously check for a wrapped negative bytes argument.
3959 DCHECK(sizeof(size_t) < 8 || bytes < (std::numeric_limits<size_t>::max() / 2));
3960 native_bytes_registered_.fetch_add(bytes, std::memory_order_relaxed);
3961 uint32_t objects_notified =
3962 native_objects_notified_.fetch_add(1, std::memory_order_relaxed);
3963 if (objects_notified % kNotifyNativeInterval == kNotifyNativeInterval - 1
3964 || bytes > kCheckImmediatelyThreshold) {
3965 CheckGCForNative(ThreadForEnv(env));
3966 }
3967 }


2. 如何在Java物件回收時觸發native記憶體回收

NativeAllocationRegistry中主要依靠Cleaner機制完成了這個過程。關於Cleaner的細節,可以參考我的上篇部落格。

3. 實際案例

Bitmap類就是通過NativeAllocationRegistry來實現native資源自動釋放的。以下是Bitmap構造方法的一部分。

frameworks/base/graphics/java/android/graphics/Bitmap.java

155         mNativePtr = nativeBitmap;         <=========================== 通過指標值間接持有native資源
156
157 final int allocationByteCount = getAllocationByteCount(); <==== 獲取native資源的大小,如果是mmap方式,這個大小最終會計入native_bytes_registered中
158 NativeAllocationRegistry registry;
159 if (fromMalloc) {
160 registry = NativeAllocationRegistry.createMalloced( <==== 根據native資源分配方式的不同,構造不同的NativeAllocationRegistry物件,nativeGetNativeFinalizer()返回的是native資源釋放函式的函式指標
161 Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
162 } else {
163 registry = NativeAllocationRegistry.createNonmalloced(
164 Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
165 }
166 registry.registerNativeAllocation(this, nativeBitmap); <===== 檢測是否需要GC


通過上述案例可知,當我們使用NativeAllocationRegistry來為Java類自動釋放native記憶體資源時,首先需要建立NativeAllocationRegistry物件,接著呼叫 registerNativeAllocation 方法。只此兩步,便可實現native資源的自動釋放。

既然需要兩步,那為什麼 registerNativeAllocation 不放進NativeAllocationRegistry的構造方法,這樣一步豈不是更好?原因是 registerNativeAllocation 獨立出來,便可以在native資源真正申請後再去告知GC,靈活性更高。此外,NativeAllocationRegistry中還有一個 registerNativeFree 方法與之對應,可以讓應用層在自己提前釋放native資源後告知GC。

作者:蘆半山

連結:https://juejin.im/post/6894153239907237902

關注我獲取更多知識或者投稿