再谈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;
            }
        }
    }
}

    通过断点,我们也可以还原对象的创建过程,例如:

    

    通过断点,我们也可以清晰的看到,在上面两个对象的创建过程中,都进入了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的时间。当连续点击按钮的时候,我们可以看到内存会不断的往上增加,而基本不会减少。

    通过线程的堆栈信息,我们也可以观察者两个线程正在做的事情:

     在这种情况下,线程都还在干活,没有到达崩溃的程度。但是内存的回收已经变得极其缓慢,及时手动触发GC,也无济于事,对象已经非常的多:

    如果这个时候再继续点击按钮,一旦内存回收遇到问题,就会引发崩溃,如下所示,引发了JNI ERROR (app bug): weak global reference table overflow (max=51200)的崩溃,因为weak reference对象太多,已经超过极限:

   我们再来模拟另外一种情况,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的执行时间,那么内存和最后的稳定性情况会怎么样呢?

    内存会和我们预期的一致,在前面几次点击的时候,由于finalize函数执行顺利,我们可以看到GC过程,内存没有快速上升。但是到了10次以后,内存就开始不断攀升。这个时候,我们让App静默等待,结果10s多后,就发生了超时崩溃,如下所示:

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设备上):

    我们可以看到,从首页开始,Finalizer对象一直在增加,因为这几个界面都没有销毁。而到了【天猫】界面,增加的很快。我们再来看下这些界面的主要Finalizer对象分布:

    上图我们可以看到Finalizer对象分布情况,在回到首页然后进入天猫之后,RenderNode和Matrix对象有了明显的上升。这与控件增加较多以及很多控件的图片使用了图片效果有关。上面的检测是在Nexus 6p设备上,在该设备上Finalize线程的回收还算比较及时。一旦包含大量的Finalizer对象的界面很多,在性能较差的设备上就会导致Finalizer对象的累积,影响到内存和性能,在部分极端的设备上还会引发崩溃的问题。
    除了本地报表有监控外,在后台我们也进行了整体的Finalizer对象的跟踪,能够跟踪各个界面的Finalizer对象数量,后续可以对Finalizer过高的界面进行有针对性的优化,以加快内存的回收,提升整体的性能。

    在内存的使用上,除了前面提到的熟悉内存工具和提高意识外。在我们写代码的时候,也要加强Finalizer对象的理解和警觉,了解哪些系统类是有Finalizer对象,并了解Finalizer对内存,性能和稳定性所带来的影响。特别是我们自己写类的时候,要尽量避免重写finalize方法,即使重写了也要注意该方法的实现,不要有耗时操作,也尽量不要抛出异常等。只有这样才能写出更加优秀的代码,才能在手淘这种超级App中运行的更加流畅和稳定。

时间: 2024-10-01 00:28:36

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

[译]再谈如何安全地在 Android 中存储令牌

本文讲的是[译]再谈如何安全地在 Android 中存储令牌, 原文地址:A follow-up on how to store tokens securely in Android 原文作者:Enrique López Mañas 译文出自:掘金翻译计划 译者: lovexiaov 校对者:luoqiuyu hackerkevin 作为本文的序言,我想对读者做一个简短的声明.下面的引言对本文的后续内容而言十分重要. 没有绝对的安全.所谓的安全是指利用一系列措施的堆积和组合,来试图延缓必然发生的

再谈矩阵分解在推荐系统中的应用

本文将简单介绍下最近学习到的矩阵分解方法. (1)PureSvd 矩阵分解的核心是将一个非常稀疏的评分矩阵分解为两个矩阵,一个表示user的特性,一个表示item的特性,将两个矩阵中各取一行和一列向量做内积就可以得到对应评分. 那么如何将一个矩阵分解为两个矩阵就是唯一的问题了.说到这里大家就可能想起了在线代和数值分析中学到的各种矩阵分解方法,QR,Jordan,三角分解,SVD... 这里说说svd分解. svd是将一个任意实矩阵分解为三个矩阵U,S,V,其中,U,V是两个正交矩阵,称为左右奇异

再谈Javascript中的基本类型和引用类型(推荐)_javascript技巧

一.基本类型和引用类型概述 js中数据类型的值包括:基本类型值和引用类型值 基本数据类型:undefined;null;boolean;number;string 引用类型值:保存在内存中,js不允许直接访问内存位置,因此时操作引用而不是实际对象 二.如何检测数据类型 1.基本数据类型的检测:使用typeof var s = "AAA"; alert(typeof s); //返回string 2.引用类型(对象类型)检测:使用instanceof alert(person insta

浅谈Python 对象内存占用_python

一切皆是对象 在 Python 一切皆是对象,包括所有类型的常量与变量,整型,布尔型,甚至函数. 参见stackoverflow上的一个问题 Is everything an object in python like ruby 代码中即可以验证: # everythin in python is object def fuction(): return print isinstance(True, object) print isinstance(0, object) print isinst

[译] 再谈 CSS 中的代码味道

本文讲的是[译] 再谈 CSS 中的代码味道, 原文地址:Code Smells in CSS Revisited 原文作者:Harry 译文出自:掘金翻译计划 译者:IridescentMia 校对者:rccoder, Germxu 再谈 CSS 中的代码味道 回到 2012 年,我写了一篇关于潜在 CSS 反模式的文章 CSS中的代码味道.回看那篇文章,尽管四年过去了,我依然认同里面的全部内容,但是我有一些新的东西加到列表中.再次说明,这些内容并不一定总是坏的东西,因此把它们称为代码味道:在

用户交互设计:再谈人机交互中的设计隐喻

文章描述:再谈人机交互中的设计隐喻. 上篇文章<人机交互中的设计隐喻-由Mac的文件替换引出来的话题>发出来以后收到了各种各样的反馈,我索性再写一篇续文,算是集中答复吧. 用户习惯 在所有的反馈中,"用户觉得Windows的做法更好用,所以理应这样设计"的说法可谓最多.那么我们就来看一下,为什么有人会觉得Windows的做法更"好用". 我们来看两个例子. 银行里面用的系统-就是柜台后面业务人员用的那个-基本上还是字符界面,没有漂亮的图标和窗口,甚至可能

c++在类的方法中被实例化的对象能自动释放内存么

问题描述 c++在类的方法中被实例化的对象能自动释放内存么 我没有使用new,就是最一般的实例化,如果没有的要怎么释放,求大神教具体的方法 解决方案 没有使用new,就是最一般的实例化,系统会自动释放的. 解决方案二: 当对象的生命周期终止时会自动释放对象所占用的内存. new声明的对象保存在堆中,直到调用delete时生命周期才终止.对象销毁内存被释放. 而直接创建的对象是保存在局部栈中,出了大括号,生命周期就终止了,对象销毁自动释放内存.函数体就是用一个大括号包起来的,函数内声明的对象,出了

类 c++ 面向对象-c++在类的方法中被实例化的对象能自动释放内存么

问题描述 c++在类的方法中被实例化的对象能自动释放内存么 我没有使用new,就是最一般的实例化,如果没有的要怎么释放,求大神教具体的方法 解决方案 如果没有new,那么是栈上变量,它除了生命期就会自动释放. 解决方案二: 对象的析构函数在的对象销毁前被调用,对象何时销毁也与其作用域有关. 例如,全局对象是在程序运行结束时销毁,自动对象是在离开其作用域时销毁,而动态对象则是在使用delete运算符时销毁. 解决方案三: 可以自动释放.你可以看一下c++内存管理:http://www.cnblog

vba-我想在VBA中将一个任意对象复制到一块内存里,或存入数据库中,咋办?

问题描述 我想在VBA中将一个任意对象复制到一块内存里,或存入数据库中,咋办? 比如,我在EXCEL中,写了如下: private sub test() dim temp() as byte '如下一句是我想象的,我想将thisWorkBook.Sheet1对象复制到temp数组 Redim temp(对象长度) CopyMemory ptr(temp(0)), ptr(sheet1), 对象长度 '如此这般,我就可以将这个对象写入数据库中了 end sub 但应该怎么做呢,C++用久了我觉得这