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

目录
相关文章
|
6月前
|
存储 缓存 网络协议
阿里云内存型实例规格性能、价格、适用场景与选型指南参考
阿里云服务器ECS(Elastic Compute Service)提供了多样化的内存型实例规格族,专为需要高性能内存资源的应用场景设计。从最新的r8a系列到经过优化的re6p系列,阿里云内存型实例旨在提供稳定、高效且安全的计算环境。这些实例不仅具备强大的计算性能与内存配比,还通过支持ESSD云盘和高效网络协议,显著提升了存储I/O能力和网络带宽,适用于大数据分析、高性能数据库、内存密集型应用等多种场景。本文将详细解析阿里云ECS中的多个内存型实例规格族,包括它们的核心特点、适用场景、实例规格及具体指标数据,为用户在选型时提供参考。
阿里云内存型实例规格性能、价格、适用场景与选型指南参考
|
2月前
|
消息中间件 存储 关系型数据库
千亿消息“过眼云烟”?Kafka把硬盘当内存用的性能魔法,全靠这一手!
Apache Kafka 是由 LinkedIn 开发并捐赠给 Apache 基金会的分布式消息队列系统,具备高吞吐、可扩展和容错能力。其核心设计围绕主题、分区、分段和偏移量展开,通过顺序写入磁盘和 Page Cache 提升性能,广泛应用于大数据实时处理场景。
101 0
|
4月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
322 0
|
5月前
|
存储 缓存 分布式计算
高内存场景必读!阿里云r7/r9i/r8y/r8i实例架构、性能、价格多维度对比
阿里云针对高性能需求场景,一般会在活动中推出内存型r7、内存型r9i、内存型r8y和内存型r8i这几款内存型实例规格的云服务器。相比于活动内的经济型e和通用算力型u1等实例规格,这些内存型实例在性能上更为强劲,尤其适合对内存和计算能力有较高要求的应用场景。这些实例规格的云服务器在处理器与内存的配比上大多为1:8,但它们在处理器架构、存储性能、网络能力以及安全特性等方面各有千秋,因此适用场景也各不相同。本文将为大家详细介绍内存型r7、r9i、r8y、r8i实例的性能、适用场景的区别以及选择参考。
|
弹性计算 安全 数据库
【转】云服务器虚拟化内存优化指南:提升性能的7个关键策略
作为云计算服务核心组件,虚拟化内存管理直接影响业务系统性能表现。本文详解了内存优化方案与技术实践,助您降低30%资源浪费。
114 0
【转】云服务器虚拟化内存优化指南:提升性能的7个关键策略
|
6月前
|
存储 分布式计算 安全
阿里云服务器内存型实例怎么选?r7/r8y/r8i实例性能、适用场景与选择参考
在选择阿里云服务器时,针对内存密集型应用和数据库应用,内存型实例因其高内存配比和优化的性能表现,成为了众多用户的热门选择。在目前阿里云的活动中,内存型实例主要有内存型r7、内存型r8y和内存型r8i实例可选。为了帮助大家更好地了解这三款实例的区别,本文将详细对比它们的实例规格、CPU、内存、计算、存储、网络等方面的性能,并附上活动价格对比,以便用户能够全面了解它们之间的不同,以供选择和参考。
|
5月前
|
存储 缓存 数据挖掘
阿里云服务器实例选购指南:经济型、通用算力型、计算型、通用型、内存型性能与适用场景解析
当我们在通过阿里云的活动页面挑选云服务器时,相同配置的云服务器通常会有多种不同的实例供我们选择,并且它们之间的价格差异较为明显。这是因为不同实例规格所采用的处理器存在差异,其底层架构也各不相同,比如常见的X86计算架构和Arm计算架构。正因如此,不同实例的云服务器在性能表现以及适用场景方面都各有特点。为了帮助大家在众多实例中做出更合适的选择,本文将针对阿里云服务器的经济型、通用算力型、计算型、通用型和内存型实例,介绍它们的性能特性以及对应的使用场景,以供大家参考和选择。
|
7月前
|
弹性计算 固态存储 ice
阿里云服务器ECS内存型2核16G、4核32G和8核64G配置实例、费用和性能参数表
本文整理了2025年阿里云服务器租赁价格表,涵盖2核16G、4核32G和8核64G配置收费标准。CPU内存比为1:8,提供多种实例规格如ECS内存型r8i、通用算力型u1等。价格由CPU内存、公网带宽及系统盘组成,支持优惠折扣(年付6.7折起)。文中详细列出各配置参考价格、公网带宽与系统盘收费,并对比不同实例规格性能,如Intel Xeon和AMD EPYC处理器系列,帮助用户选择高性价比方案。具体价格以阿里云官网为准。
935 4
|
12月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
405 77

热门文章

最新文章

下一篇
开通oss服务