应用与系统稳定性第六篇---JVM垃圾回收之finalize执行时引起timed out 闪退分析

一、背景

java.util.concurrent.TimeoutException: android.content.res.AssetManager$AssetInputStream.finalize() timed out after 10 seconds
 at android.content.res.AssetManager$AssetInputStream.close(AssetManager.java:812)
 at android.content.res.AssetManager$AssetInputStream.finalize(AssetManager.java:845)
 at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:202)
 at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:185)
 at java.lang.Thread.run(Thread.java:833)

这是一个高频问题,必须要诊治一下了,去查看一下这个crash在我们发出去的最新版本上的上报情况,看到所有重写finalize方法的对象,都有可能发生这个异常,堆栈有几十种,其中AssetManager是个大户,发生了289次,ThreadedRenderer发生了33次。其余的类大部分都是我们自己的,比如CursorWindow,SQliteCursor等

二、初步分析

android.content.res.AssetManager$AssetInputStream.finalize() timed out after 10 seconds,从报错的直观意思上来看,是由于finalize方法超时了,下面来模拟一下。

class MyObject extends View {
       public MyObject(Context context) {
           super(context);
       }
       @Override
       protected void finalize() throws Throwable {
           System.out.println("WANG  finalize begain");
           try{
               Thread.sleep(2000);
           }finally {
               super.finalize();
               System.out.println("WANG  finalize end");
           }
       }
   }

view.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               for (int i = 0; i < 1000; i++) {
                  MyObject object= new MyObject(MainActivity.this);
               }
           }
       });

当每次被GC的时候,MyObject可以被正常的回收,如下图。


image.png

看一下FinalizerWatchdogDaemon线程的trace,Sleeping状态,可能抓的时候,正处于sleep。

"FinalizerWatchdogDaemon" daemon prio=5 tid=5 Sleeping
 | group="system" sCount=1 dsCount=0 flags=1 obj=0x12f40350 self=0x6fad268c00
 | sysTid=1347 nice=4 cgrp=default sched=0/0 handle=0x6f95d264f0
 | state=S schedstat=( 118415628 218405939 1140 ) utm=7 stm=4 core=6 HZ=100
 | stack=0x6f95c23000-0x6f95c25000 stackSize=1041KB
 | held mutexes=
 at java.lang.Thread.sleep(Native method)
 - sleeping on <0x0edad885> (a java.lang.Object)
 at java.lang.Thread.sleep(Thread.java:373)
 - locked <0x0edad885> (a java.lang.Object)
 at java.lang.Thread.sleep(Thread.java:314)
 at java.lang.Daemons$FinalizerWatchdogDaemon.sleepFor(Daemons.java:344)
 at java.lang.Daemons$FinalizerWatchdogDaemon.waitForFinalization(Daemons.java:366)
 at java.lang.Daemons$FinalizerWatchdogDaemon.runInternal(Daemons.java:283)
 at java.lang.Daemons$Daemon.run(Daemons.java:105)
 at java.lang.Thread.run(Thread.java:764)

现在修改一下程序,把睡眠2s改成睡眠20s,前台运行一段时间之后,突然崩溃了(和机型相关,有一些新出来的机型,系统已经优化,这个问题就不会再出现),FinalizerWatchdogDaemon线程报错了。

04-23 15:27:57.153 26407-26416/com.example.myapplication E/AndroidRuntime: FATAL EXCEPTION: FinalizerWatchdogDaemon
   Process: com.example.myapplication, PID: 26407
   java.util.concurrent.TimeoutException: com.example.myapplication.MainActivity$MyObject.finalize() timed out after 10 seconds
       at java.lang.Thread.sleep(Native Method)
       at java.lang.Thread.sleep(Thread.java:1031)
       at java.lang.Thread.sleep(Thread.java:985)
       at com.example.myapplication.MainActivity$MyObject.finalize(MainActivity.java:36)
       at java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:202)
       at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:185)
       at java.lang.Thread.run(Thread.java:818)

所以我们知道,这个crash和finalize的耗时有一定关系,那我们在看看我们的代码是怎么写的?

256      /**
257       * Release the native resources, if they haven't been released yet.
258       */
259      @Override
260      protected void finalize() {
261          try {
262              // if the cursor hasn't been closed yet, close it first
263              if (mWindow != null) {
264                  close();
265              }
266          } finally {
267              super.finalize();
268          }
269      }
138      @Override
139      protected void finalize() throws Throwable {
140          try {
141              dispose();
142          } finally {
143              super.finalize();
144          }
145      }
146  
147      private void dispose() {
148          if (mWindowPtr != 0) {
149              recordClosingOfWindow(mWindowPtr);
150              nativeDispose(mWindowPtr);
151              mWindowPtr = 0;
152          }
153      }
154  

由此可见,finalize中都做一些额外的工作,正是这些额外的任务,在系统资源紧张,GC的时候很繁忙导致Finalizer对象回收超时,触发了App的crash。

三、深入分析

FinalizerWatchdogDaemon线程顾名思义,带有一个watchdog,说明和一个看门狗的性质是一样的,超过一定的时候不喂狗,就会被狗咬,看看源码是不是这个样子的。

http://androidxref.com/9.0.0_r3/xref/libcore/libart/src/main/java/java/lang/Daemons.java#37

30/**
31 * Calls Object.finalize() on objects in the finalizer reference queue. The VM
32 * will abort if any finalize() call takes more than the maximum finalize time
33 * to complete.
34 *
35 * @hide
36 */
37public final class Daemons {
38    private static final int NANOS_PER_MILLI = 1000 * 1000;
39    private static final int NANOS_PER_SECOND = NANOS_PER_MILLI * 1000;
40    private static final long MAX_FINALIZE_NANOS = 10L * NANOS_PER_SECOND;
41
       //开启四个线程
42    public static void start() {
43        ReferenceQueueDaemon.INSTANCE.start();
44        FinalizerDaemon.INSTANCE.start();
45        FinalizerWatchdogDaemon.INSTANCE.start();
46        HeapTaskDaemon.INSTANCE.start();
47    }
48
55    //停止4个线程
56    public static void stop() {
57        HeapTaskDaemon.INSTANCE.stop();
58        ReferenceQueueDaemon.INSTANCE.stop();
59        FinalizerDaemon.INSTANCE.stop();
60        FinalizerWatchdogDaemon.INSTANCE.stop();
61    }
   ......
62}

Daemons类的开头注释很清楚,从finalizer reference queue(终结引用队列)中取出对象,调用对象的finalize()方法的时候.如果超出了最大终结时间(一般为MAX_FINALIZE_NANOS,值是10秒),JVM就会中止。这个类中开启了4个线程。

FinalizerDaemon:对于重写了成员函数finalize的对象,它们被GC决定回收时,并没有马上被回收,而是被放入到一个队列中,等待FinalizerDaemon守护线程去调用它们的成员函数finalize,然后再被回收。

FinalizerWatchdogDaemon:与上面的线程对应,用来监控FinalizerDaemon线程的执行。一旦检测那些重写了finalize的对象在执行成员函数finalize时超出一定时间,那么就会退出JVM。

我们来看看FinalizerDaemon线程的实现。

193    private static class FinalizerDaemon extends Daemon {
194        private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();
195        private final ReferenceQueue<Object> queue = FinalizerReference.queue;
196        private final AtomicInteger progressCounter = new AtomicInteger(0);
197        // Object (not reference!) being finalized. Accesses may race!
198        private Object finalizingObject = null;
199
200        FinalizerDaemon() {
201            super("FinalizerDaemon");
202        }
203
204        @Override public void runInternal() {
205            // This loop may be performance critical, since we need to keep up with mutator
206            // generation of finalizable objects.
207            // We minimize the amount of work we do per finalizable object. For example, we avoid
208            // reading the current time here, since that involves a kernel call per object.  We
209            // limit fast path communication with FinalizerWatchDogDaemon to what's unavoidable: A
210            // non-volatile store to communicate the current finalizable object, e.g. for
211            // reporting, and a release store (lazySet) to a counter.
212            // We do stop the  FinalizerWatchDogDaemon if we have nothing to do for a
213            // potentially extended period.  This prevents the device from waking up regularly
214            // during idle times.
215
216            // Local copy of progressCounter; saves a fence per increment on ARM and MIPS.
217            int localProgressCounter = progressCounter.get();
218
219            while (isRunning()) {
220                try {
221                    // Use non-blocking poll to avoid FinalizerWatchdogDaemon communication
222                    // when busy.
223                    FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
224                    if (finalizingReference != null) {
225                        finalizingObject = finalizingReference.get();
226                        progressCounter.lazySet(++localProgressCounter);
227                    } else {
228                        finalizingObject = null;
229                        progressCounter.lazySet(++localProgressCounter);
230                        // Slow path; block.
231                        FinalizerWatchdogDaemon.INSTANCE.goToSleep();
232                        finalizingReference = (FinalizerReference<?>)queue.remove();
233                        finalizingObject = finalizingReference.get();
234                        progressCounter.set(++localProgressCounter);
235                        FinalizerWatchdogDaemon.INSTANCE.wakeUp();
236                    }
237                    doFinalize(finalizingReference);
238                } catch (InterruptedException ignored) {
239                } catch (OutOfMemoryError ignored) {
240                }
241            }
242        }
243
244        @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")
245        private void doFinalize(FinalizerReference<?> reference) {
246            FinalizerReference.remove(reference);
247            Object object = reference.get();
248            reference.clear();
249            try {
                   //上面从reference取出对象,调用它的finalize
250                object.finalize();
251            } catch (Throwable ex) {
252                // The RI silently swallows these, but Android has always logged.
253                System.logE("Uncaught exception thrown by finalizer", ex);
254            } finally {
255                // Done finalizing, stop holding the object as live.
256                finalizingObject = null;
257            }
258        }

在看看FinalizerWatchdogDaemon线程的实现

261    /**
262     * The watchdog exits the VM if the finalizer ever gets stuck. We consider
263     * the finalizer to be stuck if it spends more than MAX_FINALIZATION_MILLIS
264     * on one instance.
265     */
266    private static class FinalizerWatchdogDaemon extends Daemon {
267        private static final FinalizerWatchdogDaemon INSTANCE = new FinalizerWatchdogDaemon();
268
269        private boolean needToWork = true;  // Only accessed in synchronized methods.
270
271        FinalizerWatchdogDaemon() {
272            super("FinalizerWatchdogDaemon");
273        }
274
275        @Override public void runInternal() {
276            while (isRunning()) {
277                if (!sleepUntilNeeded()) {
278                    // We have been interrupted, need to see if this daemon has been stopped.
279                    continue;
280                }
281                final Object finalizing = waitForFinalization();
282                if (finalizing != null && !VMRuntime.getRuntime().isDebuggerActive()) {
283                    finalizerTimedOut(finalizing);
284                    break;
285                }
286            }
287        }

waitForFinalization方法用来监控FinalizerDaemon线程的回收,如果超时waitForFinalization方法返回值就不为空,触发了finalizerTimedOut方法的执行,进程被杀死,JVM退出。

396        private static void finalizerTimedOut(Object object) {
397            // The current object has exceeded the finalization deadline; abort!
398            String message = object.getClass().getName() + ".finalize() timed out after "
399                    + (MAX_FINALIZE_NANOS / NANOS_PER_SECOND) + " seconds";
400            Exception syntheticException = new TimeoutException(message);
401            // We use the stack from where finalize() was running to show where it was stuck.
402            syntheticException.setStackTrace(FinalizerDaemon.INSTANCE.getStackTrace());
403
404            // Send SIGQUIT to get native stack traces.
405            try {
                  //杀死进程
406                Os.kill(Os.getpid(), OsConstants.SIGQUIT);
407                // Sleep a few seconds to let the stack traces print.
408                Thread.sleep(5000);
409            } catch (Exception e) {
410                System.logE("failed to send SIGQUIT", e);
411            } catch (OutOfMemoryError ignored) {
412                // May occur while trying to allocate the exception.
413            }
426            if (Thread.getUncaughtExceptionPreHandler() == null &&
427                    Thread.getDefaultUncaughtExceptionHandler() == null) {
428                // If we have no handler, log and exit.
429                System.logE(message, syntheticException);
430                System.exit(2);
431            }
436            Thread.currentThread().dispatchUncaughtException(syntheticException);
437        }
438    }

从Android 5.0开始,每个View都包含了一个或者多个的Finalizer对象,如果页面的View比较多,甚至还有Activity等对象泄漏的话,那么FinalizerDaemon线程需要检测的对象越来越多,负担可想而知,在低端设备上,有可能就会来不及回收而引起性能和稳定性问题,给了FinalizerWatchdogDaemon线程可乘之机.

四、解决方案

从第三部分我们知道这个问题的RootCasue是对象回收超时,那么修复的方法基本上有两种,一种是破坏FinalizerWatchdogDaemon线程,使之stop。

  public void fix1() {
        try {
            Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");

            Method method = clazz.getSuperclass().getDeclaredMethod("stop");
            method.setAccessible(true);

            Field field = clazz.getDeclaredField("INSTANCE");
            field.setAccessible(true);

            method.invoke(field.get(null));

        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

这种方案从DEMO中确实可以解决timeout的异常问题,但是风险未知。

另外想法一种是加长时间到60s,如下

 private static final int NANOS_PER_MILLI = 1000 * 1000;
    private static final int NANOS_PER_SECOND = NANOS_PER_MILLI * 1000;
    private static final long MAX_FINALIZE_NANOS = 10L * NANOS_PER_SECOND;

    public void fix2() {
        try {
            Class<?> c = Class.forName("java.lang.Daemons");
            Field maxField = c.getDeclaredField("MAX_FINALIZE_NANOS");
            maxField.setAccessible(true);
            maxField.set(null, MAX_FINALIZE_NANOS * 6);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

第二种方案我在做ROM的时候,改过Daemons.java的源码,直接改成了60秒,全机型,未发现风险,这可能也是为什么出来的错误有的是timed out after 30 seconds的原因,但是由于MAX_FINALIZE_NANOS是final的string常量,并不能用反射,这种思路在App中行不通。

综上,我修复方案是,对于已经国内ROM已经把MAX_FINALIZE_NANOS改变大于30s的的情况,暂时不动。对于仍然是10s的,stop掉FinalizerWatchdogDaemon线程,为了风险可控,加上了云控开关,可以随时控制关闭FinalizerWatchdogDaemon线程这个功能是否打开。

最后给出个人的几个建议

1、不到万不得已,不要重写finalize方法

2、做好内存优化,减少GC的频繁调用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268