重載Finalize引發的內存泄露

一些同學在開發JNI的時候,通常會使用finalize來做native內存的釋放,而在頻繁的列表滑動創建和回收持有native內存的Java對象(簡稱NativeBean,後同)後,我們去使用內存分析工具,比如zprofiler、mat等,分析一些oom的heap時,經常能看到 java.lang.ref.FinalizerReference佔用很大數量的NativeBean,而其實你並沒有這麼多數據項,那麼這裡面的原因是什麼呢?

1、FinalizerReference

通過MAT這些工具你會發現,我們的NativeBean都被FinalizerReference持有者,這個FinalizerReference主要目的就是為了協助FinalizerDaemon守護線程完成對象的finalize工作而生的,我們先從代碼來看起。

public final class FinalizerReference<T> extends Reference<T> { // This queue contains those objects eligible for finalization. public static final ReferenceQueue queue = new ReferenceQueue(); .............. .............. // When the GC wants something finalized, it moves it from the "referent" field to // the "zombie" field instead. private T zombie; public FinalizerReference(T r, ReferenceQueue<? super T> q) { super(r, q); } @Override public T get() { return zombie; } @Override public void clear() { zombie = null; } public static void add(Object referent) { FinalizerReference<?> reference = new FinalizerReference(referent, queue); synchronized (LIST_LOCK) { reference.prev = null; reference.next = head; if (head != null) { head.prev = reference; } head = reference; } } public static void remove(FinalizerReference<?> reference) { synchronized (LIST_LOCK) { FinalizerReference<?> next = reference.next; FinalizerReference<?> prev = reference.prev; reference.next = null; reference.prev = null; if (prev != null) { prev.next = next; } else { head = next; } if (next != null) { next.prev = prev; } } }

這個類 提供了2個很重要的方法,add是將對象插入到ReferenceQueue中,而remove則是從中移出,而這個ReferenceQueue源碼比較簡單,大家可以自行去了解一下,其實可以簡單理解是引用的隊列,那麼我們重點要了解清楚的是這些引用是什麼時機添加到ReferenceQueue,什麼時機又從中移出呢?

2、Add和Remove的時機

在我們了解時機之前,我們先學習一些基本概念,什麼是finalizer類呢?其實我們知道類的修飾有很多,比如final,abstract,public等,如果某個類用final修飾,我們就說這個類是final類,上面列的都是語法層面我們可以顯式指定的,在JVM里其實還會給類標記一些其他符號,比如finalizer,表示這個類是一個finalizer類(為了和java.lang.ref.Finalizer類區分,下文在提到的帶finalizer標識的類時會簡稱為f類),GC在處理這種類的對象時要做一些特殊的處理,如在這個對象被回收之前會調用它的finalize方法。

也就是說重載了finalize函數,並且非空實現的類就是咱們所說的f類,接下來我們就聊聊add的時機。

了解LeakCanary原理的同學,對這個機制應該比較了解,當WeakReference 創建時,傳入一個 ReferenceQueue 對象,當被 WeakReference 引用的對象的生命周期結束,一旦被 GC 檢查到,GC 將會把該對象添加到 ReferenceQueue 中。那麼對於FinalizerReference來說,他其實也是類似的,這個add方法是從虛擬機中反調回來的,當GC發生時queue中就會插入當前正準備釋放內存的對象的f類引用。

那麼f類又是在什麼時機從ReferenceQueue中移出的呢?

public final class Daemons { ... private static class FinalizerDaemon extends Daemon { private static final FinalizerDaemon INSTANCE = new FinalizerDaemon(); private final ReferenceQueue<Object> queue = FinalizerReference.queue; private volatile Object finalizingObject; private volatile long finalizingStartedNanos; FinalizerDaemon() { super("FinalizerDaemon"); } @Override public void run() { while (isRunning()) { // Take a reference, blocking until one is ready or the thread should stop try { doFinalize((FinalizerReference<?>) queue.remove()); } catch (InterruptedException ignored) { } } } @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION") private void doFinalize(FinalizerReference<?> reference) { FinalizerReference.remove(reference); Object object = reference.get(); reference.clear(); try { finalizingStartedNanos = System.nanoTime(); finalizingObject = object; synchronized (FinalizerWatchdogDaemon.INSTANCE) { FinalizerWatchdogDaemon.INSTANCE.notify(); } object.finalize(); } catch (Throwable ex) { // The RI silently swallows these, but Android has always logged. System.logE("Uncaught exception thrown by finalizer", ex); } finally { // Done finalizing, stop holding the object as live. finalizingObject = null; } } } ...}

FinalizerDaemon是Daemons.java中定義的另一個守護線程,FinalizerReference中定義的queue的消費者就是它。它內部定義了一個ReferenceQueue類型的對象queue,並將其賦值為前面說的FinalizerReference中的定義的那個queue。run方法中通過ReferenceQueue的remove方法把保存在queue中的Reference獲取出來並通過doFinalize方法來調用f類的finalize方法,這裡我們就了解到了Remove時機,不過我這裡還多說一點,說說一個我們遇到的finalize timed out異常的原理。

通過查看ReferenceQueue的源碼可知,ReferenceQueue的remove方法是阻塞的,在隊列中沒有Reference時將阻塞直到有Reference入隊。我們看一下doFinalize方法,通過從隊列中獲取出來的reference的get方法獲取到被引用的真實對象,並在這裡調用該對象的finalize方法。但在這之前會通過FinalizerWatchdogDaemon.INSTANCE.notify()喚醒FinalizerWatchdogDaemon守護線程,而這個FinalizerWatchdogDaemon它主要用來監控finalize方法執行的時長,並在finalize執行超時時會拋出,所以我們不要在finalize方法中做耗時操作。

3、f類的使用不當造成的影響

首先當然是我們要解決的內存泄露,由於Daemons中的幾個都是守護線程,我們看到它會創建一個,這個線程的優先順序並不是最高的,意味著在CPU很緊張的情況下其被調度的優先順序可能會受到影響,所以當你在頻繁創建f類對象時,他沒有辦法及時被回收,造成內存泄露。

比如當你在adapter中的getview去創建這個f類的時候,而當f類要被回收時他會首先加入到ReferenceQueue中,當你不斷滑動列表去繪製,CPU資源緊張的情況下,這個守護線程沒有被調度去消費這些存在ReferenceQueue中的f類,這樣就有可能造成內存泄露。

與此同時由於第一次GC的時候會將對象加入到ReferenceQueue中來,導致f類的回收至少需要2次GC才能被回收,而守護線程的優先順序低,很可能長時間沒被回收,從而容易導致f類在資源緊張時進入到老年代,從而引起full gc造成卡頓。

4、解決策略

盡量不要重載finalize方法,而是通過自己業務的監控或者手動介面去釋放內存,如果一定要使用,那麼一定不要讓這些頻繁創建的對象,或者大對象通過finalize來釋放,finalize最好是作為最後的保證。

如果一定要使用finalize方法,要記得調用super.finalize

參考文獻:

infoq.com/cn/articles/j

bozhiyue.com/anroid/bok

PS:發這麼篇水文的目的是下一篇內存優化要引用到,就先發出來了。。。


推薦閱讀:

Google 新推出的 Allo 聊天應用,可能要佔領全宇宙了
從0開始學習 GitHub 系列之「Git 速成」
能夠買到的話,你選擇魅藍metal,樂1s,榮耀5x這其中的哪個,或者同價位你會選擇哪個??
鎚子手機的整容臉和羅永浩的自我救贖:M1系列會被市場認可嗎?
蘋果和微軟沒有開源,Linux 和 Android 開源,為什麼前者幾乎沒聽說過病毒隱患而後者比較嚴重?

TAG:Android | Android内存 | 内存优化 |