應用與系統穩定性第六篇---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閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373