閱讀699 返回首頁    go 阿裏雲 go 技術社區[雲棲]


再談Finalizer對象--大型App中內存與性能的隱性殺手

    在上一篇《提升Android下內存的使用意識和排查能力》的文章中,多次提到了Finalizer對象。也可以看到該對象的清理至少是需要兩次GC才能完成,而在Android5.0,尤其是6.0以後的係統中,對於該對象的回收變得更加的慢。我們在開發的時候往往關注內存的分配、泄漏,卻容易忽視Finalizer對象,其實在大型App中,該對象是引起內存和性能問題的一個不可忽視的元凶。在類似於雙十一會場的界麵中,在使用一段時間後,設備會變得越來越慢,內存使用量也不斷攀升,甚至容易引發OOM,這個有一個重要原因就和Finalizer對象的過度使用有關。為什麼過度的使用Finalizer對象會對性能和內存都造成危害呢?我們不妨來看下Finalizer對象的原理。

一、Finalizer對象創建過程帶來的開銷

    Finalizer對象是指Java類中重寫了finalize方法,且該方法不為空的對象。當運行時環境遇到創建Finalizer對象的時候,既創建對象實例的時候,會先判斷該對象是否是Finalizer對象,如果是,那麼在構造函數過程中會把生成的對象再封裝成Finalizer對象並添加到 Finalizer鏈表中。在運行時環境中,也會有一個專門的FinalizerReference來處理和Finalizer對象的關聯。我們可以看一下Android 7.0上的FinalizerReference的代碼:
public final class FinalizerReference<T> extends Reference<T> {
    // This queue contains those objects eligible for finalization.
    public static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();

    // Guards the list (not the queue).
    private static final Object LIST_LOCK = new Object();

    // This list contains a FinalizerReference for every finalizable object in the heap.
    // Objects in this list may or may not be eligible for finalization yet.
    private static FinalizerReference<?> head = null;

    // The links used to construct the list.
    private FinalizerReference<?> prev;
    private FinalizerReference<?> next;

    // 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<Object>(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;
            }
        }
    }
}

    通過斷點,我們也可以還原對象的創建過程,例如:
1913e048c37a7d972beb3e7822c821cd143b064d

c439475c98e61ab63897a758aacbd33e2d1be056    


    通過斷點,我們也可以清晰的看到,在上麵兩個對象的創建過程中,都進入了FinalizerReference的add函數。在該函數中,又會增加一個包裝的對象FinalizerReference,這本身就是對內存的一個開銷。另外,從上麵的代碼,我們很容易看到一個問題,在add和remove的時候,都會遇到synchronized (LIST_LOCK)的同步鎖問題。當大量的這種類型的對象需要同時創建或者回收的時候,就會遇到線程間的鎖開銷問題。在一個大型app中,這是不得不考慮的因素。而在Android4.2之前,同步對象用的是class本身,也就是鎖的粒度會更大,當係統中有不止一個FinalizerReference對象的時候性能開銷會更大。另外,在添加對象的時候,在隊列中也會遇到另外一個鎖,下麵代碼中會分析到。

二、Finalizer對象回收過程帶來的開銷和問題

    在Android係統中,會有一個專門的線程來實現該對象的回收。我們在查看線程的時候就可以看到有這樣一個FinalizerDaemon線程。

1、額外增加的多個同步鎖開銷

首先先看下該線程的代碼:

 public final class Daemons {
    public static void start() {
        ReferenceQueueDaemon.INSTANCE.start();
        FinalizerDaemon.INSTANCE.start();
        FinalizerWatchdogDaemon.INSTANCE.start();
        HeapTaskDaemon.INSTANCE.start();
    }

    public static void stop() {
        HeapTaskDaemon.INSTANCE.stop();
        ReferenceQueueDaemon.INSTANCE.stop();
        FinalizerDaemon.INSTANCE.stop();
        FinalizerWatchdogDaemon.INSTANCE.stop();
    }
......
}

 private static class FinalizerDaemon extends Daemon {
        private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();
        private final ReferenceQueue<Object> queue = FinalizerReference.queue;
        private final AtomicInteger progressCounter = new AtomicInteger(0);
        // Object (not reference!) being finalized. Accesses may race!
        private Object finalizingObject = null;

        FinalizerDaemon() {
            super("FinalizerDaemon");
        }

        @Override public void run() {


            while (isRunning()) {
                try {
                    // Use non-blocking poll to avoid FinalizerWatchdogDaemon communication
                    // when busy.
                    FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
                    if (finalizingReference != null) {
                        finalizingObject = finalizingReference.get();
                        progressCounter.lazySet(++localProgressCounter);
                    } else {
                        finalizingObject = null;
                        progressCounter.lazySet(++localProgressCounter);
                        // Slow path; block.
                        FinalizerWatchdogDaemon.INSTANCE.goToSleep();
                        finalizingReference = (FinalizerReference<?>)queue.remove();
                        finalizingObject = finalizingReference.get();
                        progressCounter.set(++localProgressCounter);
                        FinalizerWatchdogDaemon.INSTANCE.wakeUp();
                    }
                    doFinalize(finalizingReference);
                } catch (InterruptedException ignored) {
                } catch (OutOfMemoryError ignored) {
                }
            }
        }

        @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")
        private void doFinalize(FinalizerReference<?> reference) {
            FinalizerReference.remove(reference);
            Object object = reference.get();
            reference.clear();
            try {
                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線程和該線程的守護線程。在前麵的代碼中我們可以看到,在Finalizer對象add的時候,會關聯到一個ReferenceQueue的queue中。在該線程進行處理這些對象的時候,首先會從ReferenceQueue的隊列中獲取鏈表的頭結點。我看可以看下poll方法的代碼:
  public Reference<? extends T> poll() {
        synchronized (lock) {
            if (head == null)
                return null;

            return reallyPollLocked();
        }
    }
    從這裏我們可以看到,這裏會遇到另外一個鎖lock, 該鎖和FinalizerReference代碼中的鎖是獨立的。我們可以看到,在doFinalize函數中,會首先調用FinalizerReference對象的remove方法,該方法前麵已經可以看到存在在同步鎖。也就是在加入和刪除Finalizer對象的時候會同時遇到這兩個鎖開銷。

2、難以預知的finalize方法調用開銷

    在doFinalize函數中,我們可以看到,對該對象的finalize方法的調用。這裏看似沒有問題,但是一旦該對象的finalize寫法有問題:耗時、進入其他資源、不斷拋出異常等待等等就會遇到問題。這些都會引起本身該代碼的性能問題,更進一步會影響到整個App中的Finalizer對象的內存回收,一旦內存回收不過來,係統就會引發崩潰。
    在係統中還有一個FinalizerWatchdogDaemon的守護進程,該進程會監控FinalizerDaemon線程的運行,一旦FinalizerDaemon在處理一個對象的時候超過10s中,那麼就會結束進程,導致崩潰。我們可以查看FinalizerWatchdogDaemon的主要代碼:

private static class FinalizerWatchdogDaemon extends Daemon {
        private static final FinalizerWatchdogDaemon INSTANCE = new FinalizerWatchdogDaemon();

        private boolean needToWork = true;  // Only accessed in synchronized methods.

       FinalizerWatchdogDaemon() {
            super("FinalizerWatchdogDaemon");
        }

        @Override public void run() {
            while (isRunning()) {
                if (!sleepUntilNeeded()) {
                    // We have been interrupted, need to see if this daemon has been stopped.
                    continue;
                }
                final Object finalizing = waitForFinalization();
                if (finalizing != null && !VMRuntime.getRuntime().isDebuggerActive()) {
                    finalizerTimedOut(finalizing);
                    break;
                }
            }
        }

        private static void finalizerTimedOut(Object object) {
......
            Thread.UncaughtExceptionHandler h = Thread.getDefaultUncaughtExceptionHandler();
            // Send SIGQUIT to get native stack traces.
            try {
                Os.kill(Os.getpid(), OsConstants.SIGQUIT);
                // Sleep a few seconds to let the stack traces print.
                Thread.sleep(5000);
            } catch (Exception e) {
                System.logE("failed to send SIGQUIT", e);
            } catch (OutOfMemoryError ignored) {
                // May occur while trying to allocate the exception.
            }
            if (h == null) {
                // If we have no handler, log and exit.
                System.logE(message, syntheticException);
                System.exit(2);
            }

            h.uncaughtException(Thread.currentThread(), syntheticException);
        }
    }
}

     因為finalize方法調用的不確定性,所以不僅僅會導致性能問題,還會引起內存問題和穩定性問題。

3、finalize帶來的內存和穩定性問題


     我們通過代碼來模擬一下寫法不準確帶來的危害。

    class MyView extends  View{
        public MyView(Context context) {
            super(context);
        }

        @Override
        protected void finalize() throws Throwable {
            try {
                Thread.sleep(1000);
            } finally {
                super.finalize();
            }
        }

    }

    void onButtonClick(){
        for (int i = 0; i < 1000; i++) {
            View view = new MyView(this);
        }
    }

     在點擊按鈕的時候會創建1000個View,而每個view在回收的時候都需要等待1s的時間。當連續點擊按鈕的時候,我們可以看到內存會不斷的往上增加,而基本不會減少。

634244d1ce09dac0506061cae6409b04c1f42d68

    通過線程的堆棧信息,我們也可以觀察者兩個線程正在做的事情:
75e579babf7e8d7204576722518490276507bc3c

56ff34411c5cd6dbfd05d5975f2b2bc4cb4b3658

     在這種情況下,線程都還在幹活,沒有到達崩潰的程度。但是內存的回收已經變得極其緩慢,及時手動觸發GC,也無濟於事,對象已經非常的多:

dc697146d34bd79e5850aa8d140118fc66448f34

    如果這個時候再繼續點擊按鈕,一旦內存回收遇到問題,就會引發崩潰,如下所示,引發了JNI ERROR (app bug): weak global reference table overflow (max=51200)的崩潰,因為weak reference對象太多,已經超過極限:

4539ad628fdcc18a3617a032c11c714e05cbbf51

   我們再來模擬另外一種情況,finalize函數長時間無法返回的情況。代碼如下:
   class MyView extends  View{
        int mIndex = 0;
        public MyView(Context context, int index) {
            super(context);
            mIndex = index;
        }

        @Override
        protected void finalize() throws Throwable {
            try {
                if(mIndex == 10000) {
                    Thread.sleep(20000);
                }
            } finally {
                super.finalize();
            }
        }

    }

    void onButtonClick(){
        for (int i = 0; i < 1000; i++) {
            View view = new MyView(this,count);
            count++;
        }
    }

     在index值為10000的時候,finalize函數需要20s的執行時間,那麼內存和最後的穩定性情況會怎麼樣呢?

602a6749e3e8b13513ed63dd8b6ead0a1c35f916

    內存會和我們預期的一致,在前麵幾次點擊的時候,由於finalize函數執行順利,我們可以看到GC過程,內存沒有快速上升。但是到了10次以後,內存就開始不斷攀升。這個時候,我們讓App靜默等待,結果10s多後,就發生了超時崩潰,如下所示:

d696afcfca4967246dcc36fc1fab0bda93a91d7b


4、線程優先級引入的內存和性能問題

    由於在一些設備上UI和Render線程的Nice優先級值都是負數,而該線程的Nice值一般情況下是0,也就是默認值。在UI等其他線程都繁忙的時候,finalize的回收並不會很快,這樣就會導致內存回收變慢,進一步影響到整體的性能。特別是很多低性能的設備,更加容易暴露這方麵的問題。

5、Android不同版本帶來的問題

    之前的文件已經介紹過,從Android 5.0開始,每個View都包含了一個或者多個的Finalizer對象,RenderNode對象的增加會導致一定的內存和性能問題,尤其是當一個界麵需要創建大量的控件的時候,該問題就會特別明顯,例如在手淘中的某些Weex頁麵,由於渲染界麵的樣式是過前端控製的,沒有分頁的概念,這樣一次性創建非常多的控件,並且很多控件都額外使用了其他Finalizer對象,這樣就會導致這種情況下,內存會正常很快,在低端設備上,有可能就會來不及回收而引起性能和穩定性問題。我們可以看下View和RenderNode的代碼:
public class RenderNode {
......
    @Override
    protected void finalize() throws Throwable {
        try {
            nDestroyRenderNode(mNativeRenderNode);
        } finally {
            super.finalize();
        }
   }
}

//---------------------

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {

    /**
     * RenderNode used for backgrounds.
     * <p>
     * When non-null and valid, this is expected to contain an up-to-date copy
     * of the background drawable. It is cleared on temporary detach, and reset
     * on cleanup.
     */
    private RenderNode mBackgroundRenderNode;
    /**
     * RenderNode holding View properties, potentially holding a DisplayList of View content.
     * <p>
     * When non-null and valid, this is expected to contain an up-to-date copy
     * of the View content. Its DisplayList content is cleared on temporary detach and reset on
     * cleanup.
     */
    final RenderNode mRenderNode;
}

     當然,除了View以外,Path,NinePatch,Matrix,文件操作的類,正則表達式等等都會創建Finalizer對象,在大型App中過多的使用這些操作對內存和性能和穩定性都會帶來比較大的影響。

6、對象累積帶來的問題

    如果大量的Finalizer對象累積無法及時回收,那麼我們可以預見到,FinalizerDaemon線程就會增加越來越重的負擔,在GC過程中,需要檢測的對象越來越多,所占用的CPU資源也必然增加。整體CPU占用過多,肯定也會對UI線程和業務線程產生幹擾,對性能產生影響,而且由於其占用的內存無法及時釋放,那麼整個內存的利用率和分配過程也會對性能造成影響。另外考慮到同步鎖的影響,在線程越多的情況下,在創建Finalizer對象的過程中,也會影響到使用方的線程的性能。

三、Finalizer對象的監管

    在手淘的性能體係中,有專門對Finalizer對象做了監控。在接入OnLineMonitor較新版本的App中都可以監控到Finalizer的數量和分布(統計分布的功能需要額外開啟)。例如,我們啟動手淘,點擊微淘,問大家,天貓,天貓國際這幾個界麵,在最後的報告中,我們就可以看到這些界麵的Finalizer變化,如下圖所示(Nexus 6p設備上):
6659c4312b3f33281914532009e2ca511754b94a
    我們可以看到,從首頁開始,Finalizer對象一直在增加,因為這幾個界麵都沒有銷毀。而到了【天貓】界麵,增加的很快。我們再來看下這些界麵的主要Finalizer對象分布:
8e95a6f81d76ef558807cdf7d8d8a50423a3201d


474a0e3369ded3b54fd938199e01fb81b8ff7088

    上圖我們可以看到Finalizer對象分布情況,在回到首頁然後進入天貓之後,RenderNode和Matrix對象有了明顯的上升。這與控件增加較多以及很多控件的圖片使用了圖片效果有關。上麵的檢測是在Nexus 6p設備上,在該設備上Finalize線程的回收還算比較及時。一旦包含大量的Finalizer對象的界麵很多,在性能較差的設備上就會導致Finalizer對象的累積,影響到內存和性能,在部分極端的設備上還會引發崩潰的問題。
    除了本地報表有監控外,在後台我們也進行了整體的Finalizer對象的跟蹤,能夠跟蹤各個界麵的Finalizer對象數量,後續可以對Finalizer過高的界麵進行有針對性的優化,以加快內存的回收,提升整體的性能。

    在內存的使用上,除了前麵提到的熟悉內存工具和提高意識外。在我們寫代碼的時候,也要加強Finalizer對象的理解和警覺,了解哪些係統類是有Finalizer對象,並了解Finalizer對內存,性能和穩定性所帶來的影響。特別是我們自己寫類的時候,要盡量避免重寫finalize方法,即使重寫了也要注意該方法的實現,不要有耗時操作,也盡量不要拋出異常等。隻有這樣才能寫出更加優秀的代碼,才能在手淘這種超級App中運行的更加流暢和穩定。

最後更新:2017-10-20 16:03:33

  上一篇:go  前端零基礎學習提綱
  下一篇:go  從零開始建站