再谈Finalizer对象--大型App中内存与性能的隐性杀手

  1. 云栖社区>
  2. 博客>
  3. 正文

再谈Finalizer对象--大型App中内存与性能的隐性杀手

霍迪尼 发布时间:2017-10-20 15:32:54 浏览4989 评论1

摘要:     在上一篇《提升Android下内存的使用意识和排查能力》的文章中,多次提到了Finalizer对象。也可以看到该对象的清理至少是需要两次GC才能完成,而在Android5.0,尤其是6.0以后的系统中,对于该对象的回收变得更加的慢。

    在上一篇《提升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中运行的更加流畅和稳定。

【云栖快讯】云栖专辑 | 阿里开发者们的20个感悟,一通百通  详情请点击

网友评论

1F
cactusjy

我在处理商城app主页时,使用的是viewpage+FragmentStatePagerAdapter+fragment,也出现类似finalizerReference占据高内存始终无法释放,我用android studio dump 发现了F2CFB5E6_930D_4220_BD9D_31BAE0501B01,但是我并没有找到 那个地方有重写finalize,但是还是引起内存占用过大,始终无法释放,请问您:这是如何引起的,有解决办法吗

霍迪尼

可以看下系统的类,有不少都是重写了finalize的,包括每个View都有的RenderNode,还有图片相关的一些NinePatch,Matrix,File操作的一些类。因为你用了viewpage+ fragment,可能会同时缓存好几个界面的fragment,View会比较多,如果界面又比较长那就更多了。

1520516560003372

如何监控Finalzer的对象?

1520516560003372

@霍迪尼 在我们的应用中有一个native Crash,是FinalizerDaemon 线程在执行RenderNode finalize的时候发生了crash,如何定位是哪个view 的RenderNode发生的crash?

霍迪尼

@1520516560003372 如果你的native crash的话,是不是在finalize的时候释放的资源存在问题呢?可以重新注入FinalizerDaemon线程做一些日志等方面的试试。

1520516560003372

@霍迪尼 这个是具体的Crash信息,我对FinalizerDameon进行hook,记录remove view,根据上报的信息发现是系统类的view的RenderNode在执行finalize方法时发生了crash.错误的地址是0x1.根据错误的地址来看应该是空指针造成的。

00 pc /system/lib/libskia.so (SkWriteBuffer::writeFlattenable(SkFlattenable const*) +208)01 pc /system/lib/libskia.so (SkPairPathEffect::flatten(SkWriteBuffer&) const +24)02 pc /system/lib/libskia.so (SkPath::~SkPath() +76)03 pc /system/lib/libhwui.so (android::uirenderer::RenderProperties::Property::destroy() +34)04 pc /system/lib/libhwui.so (android::uirenderer::RenderProperties::destroyAll() +22)05 pc /system/lib/libhwui.so (android::uirenderer::RenderProperties::~RenderProperties() +36)06 pc /system/lib/libhwui.so (android::uirenderer::RenderNode::~RenderNode() +104)07 pc /system/lib/libhwui.so (android::uirenderer::RenderNode::~RenderNode() +04)08 pc /system/lib/libandroid_runtime.so09 pc /data/dalvik-cache/arm/system@framework@boot.oat

java:
android.view.RenderNode.finalize(RenderNode.java:920)

评论