再谈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中运行的更加流畅和稳定。

目录
相关文章
|
1月前
|
存储 缓存 监控
|
11天前
|
C# Windows
【Azure App Service】在App Service for Windows上验证能占用的内存最大值
根据以上测验,当使用App Service内存没有达到预期的值,且应用异常日志出现OutOfMemory时,就需要检查Platform的设置是否位64bit。
36 11
|
10天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
11天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
12天前
|
监控 安全 程序员
如何使用内存池池来优化应用程序性能
如何使用内存池池来优化应用程序性能
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
65 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
15天前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
1月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
1月前
|
存储 缓存 监控
Linux中内存和性能问题
【10月更文挑战第5天】
38 4
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配