Loading...
墨滴

掀乱书页的风

2021/10/16  阅读:23  主题:兰青

android疑难杂症之finalize()超时异常

问题

bugly排名前几的大户

bugly_bug.png
bugly_bug.png

实际抛出异常为java.util.concurrent.TimeoutException,这类问题难查难解,它会出现在很多类里,不止AssetManager.finalize(),主要集中在9.0以下的系统中 bug分布.png

finialize()是什么

finalize 方法定义在 Object 类中,在 GC决定回收一个不被其他对象引用的对象时调用。子类覆写 finalize 方法来处置系统资源或是负责清除操作

finialize()执行过程

(1)当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收

具体的finalize流程:

对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义如下:

  • unfinalized: 新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的
  • finalizable: 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行
  • finalized: 表示GC已经对该对象执行过finalize方法
  • reachable: 表示GC Roots引用可达
  • finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达
  • unreachable:对象不可通过上面两种途径可达

状态变迁图

对象状态变迁.png
对象状态变迁.png

变迁说明:

  1. 新建对象首先处于[reachable, unfinalized]状态(A)
  2. 随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态
  3. 若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)。
  4. 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。该对象将变迁到(reachable, finalized)状态(J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N)
  5. 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
  6. 若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)
  7. 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)

异常原因

JAVA在内存回收的时候.并不是马上将对象销毁,而是放入到一个队列中,由FinalizerDaemon(析构守护线程)调用他们的finalize方法,再被回收,所以当FinalizerDaemon回收过程中.超过10秒,FinalizerWatchdogDaemon线程就会抛出TimeoutException XXX.finalize() timed out after 10 seconds

AssetManager$AssetInputStream.finalize()

 1 public final class AssetInputStream extends InputStream {
 2    ...
 3    public final void close() throws IOException {
 4        synchronized (AssetManager.this) {
 5            ...
 6        }
 7    }
 8    ...
 9    protected void finalize() throws Throwable {
10        close();
11    }
12    ...
13 }

AssetManager 的内部类 AssetInputStream 在执行 finalize() 方法时调用 close() 方法时需要拿到外部类 AssetManager 对象锁, 而在 AssetManager 类中几乎所有的方法运行时都需要拿到同样的锁,如果 AssetManager 连续加载了大量资源或者加载资源是耗时较长,就有可能导致内部类对象 AssetInputStream 在执行finalize() 时长时间拿不到锁而导致方法执行超时

 1 public final class AssetManager implements AutoCloseable {
 2    ...
 3    /*package*/ final CharSequence getResourceText(int ident) {
 4        synchronized (this) {
 5            ...
 6        }
 7        return null;
 8    }
 9    ...
10    public final InputStream open(String fileName, int accessMode) throws IOException {
11        synchronized (this) {
12            ...
13        }
14        throw new FileNotFoundException("Asset file: " + fileName);
15    }
16    ...
17 }

Demons的由来

Daemons开始于Zygote进程创建子进程,创建子进程的时候会启动Zygote的4个Daemon(守护)线程

  1. VM_HOOKS.preFork(),做准备工作。
  2. nativeForkAndSpecialize,创建子进程
  3. VM_HOOKS.postForkCommon();启动Zygote的4个Daemon线程,java堆整理,引用队列,以及析构线程。
    //第一步:
    //停止四个线程:Daemon线程,java堆整理,引用队列,析构线程 
    //也就是创建子进程的时候,不能有这几个线程搅和。 
    public void preFork() {
        Daemons.stop();
        waitUntilAllThreadsStopped();
        token = nativePreFork();
    }

    ....

    /**
     * Called by the zygote in both the parent and child processes after
     * every fork. In the child process, this method is called after
     * {@code postForkChild}.
     */
     //第三步,启动Daemons
    public void postForkCommon() {
        Daemons.start();
    }

Daemons类的开头写下了关键的注释,当调用finalizer reference queue(终结引用队列)中的对象的Object.finalize()方法的时候.假如有任何调用finalize() 方法时.超出了最大终结时间(一般为10秒).VM就会中止.这个最大终结时间就是MAX_FINALIZE_NANOS


/**
 * Calls Object.finalize() on objects in the finalizer reference queue. The VM
 * will abort if any finalize() call takes more than the maximum finalize time
 * to complete.
 *
 * @hide
 */
public final class Daemons {

    private static final int NANOS_PER_MILLI = 1000 * 1000;
    private static final int NANOS_PER_SECOND = NANOS_PER_MILLI * 1000;
    //finalize方法执行的超时时间10s
    private static final long MAX_FINALIZE_NANOS = 10L * NANOS_PER_SECOND;

    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();
    }

    ...
}

四个守护线程

  1. ReferenceQueueDaemon:引用队列守护线程。我们知道,在创建引用对象的时候,可以关联一个队列。当被引用对象引用的对象被GC回收的时候,被引用对象就会被加入到其创建时关联的队列去。这个加入队列的操作就是由ReferenceQueueDaemon守护线程来完成的。这样应用程序就可以知道哪些被引用的对象已经被回收了。
  2. FinalizerDaemon:析构守护线程。对于重写了成员函数finalize的对象,它们被GC决定回收时,并没有马上被回收,而是被放入到一个队列中,等待FinalizerDaemon守护线程去调用它们的成员函数finalize,然后再被回收。
  3. FinalizerWatchdogDaemon:析构监护守护线程。用来监控FinalizerDaemon线程的执行。一旦检测那些重写了finalize的对象在执行成员函数finalize时超出一定时间,那么就会退出VM。
  4. HeapTaskDaemon : 堆裁剪守护线程。用来执行裁剪堆的操作,也就是用来将那些空闲的堆内存归还给系统。

守护线程的抽象类

  /**
     * A background task that provides runtime support to the application.
     * Daemons can be stopped and started, but only so that the zygote can be a
     * single-threaded process when it forks.
     */
    private static abstract class Daemon implements Runnable {

        private Thread thread;
        private String name;

        protected Daemon(String name) {
            this.name = name;
        }

        public synchronized void start() {
            if (thread != null) {
                throw new IllegalStateException("already running");
            }
            thread = new Thread(ThreadGroup.systemThreadGroup, this, name);
            thread.setDaemon(true);
            thread.start();
        }

        public abstract void run();

        ...

}

Daemon线程是四大守护线程的抽象类.核心是在start()方法的时候,将自己放到ThreadGroup.systemThreadGroup中.并设置为守护线程,我们的重点是FinalizerDaemon和FinalizerWatchdogDaemon这两个线程

FinalizerDaemon:析构守护线程

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

FinalizerWatchdogDaemon:析构监护守护线程

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

    /**
     * The watchdog exits the VM if the finalizer ever gets stuck. We consider
     * the finalizer to be stuck if it spends more than MAX_FINALIZATION_MILLIS
     * on one instance.
     */
    private static class FinalizerWatchdogDaemon extends Daemon {
        private static final FinalizerWatchdogDaemon INSTANCE = new FinalizerWatchdogDaemon();

        FinalizerWatchdogDaemon() {
            super("FinalizerWatchdogDaemon");
        }

        @Override public void run() {
        ...
        }
}

错误日志元凶

当WatchDog线程检测finalizing!=null的时候.会提示TimeOutException.

  @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) {
            // The current object has exceeded the finalization deadline; abort!
            String message = object.getClass().getName() + ".finalize() timed out after "
                    + (MAX_FINALIZE_NANOS / NANOS_PER_SECOND) + " seconds";
            Exception syntheticException = new TimeoutException(message);
            // We use the stack from where finalize() was running to show where it was stuck.
            syntheticException.setStackTrace(FinalizerDaemon.INSTANCE.getStackTrace());
            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);
            }
            // Otherwise call the handler to do crash reporting.
            // We don't just throw because we're not the thread that
            // timed out; we're the thread that detected it.
            h.uncaughtException(Thread.currentThread(), syntheticException);
        }

解决方案

  1. 加大超时时间,国内厂商有的都改成30s了,bugly上报的120s了
    分析:Daemons 类中 的 MAX_FINALIZE_NANOS 是个long 型的静态常量,代码中出现的 MAX_FINALIZE_NANOS 字段在编译期就会被编译器替换成常量,因此运行期修改是不起作用的
    结论:不可行
  2. UncaughtExceptionHandler中忽略这个异常
    分析:发生timeout时在调用UncaughtExceptionHandler之前会发送一个SIGQUIT信号,这个信号虽然只是用来dump stack trace的,但是不少手机会将该信号判断为发生了ANR,然后弹出“应用无响应”的提示,所以并不是用户无感知的,之前app中已经采取了这种方案,效果不是很明显
    结论:效果甚微
  3. 反射停掉FinalizerWatchdogDaemon线程(滴滴Booster的方案)
    顾名思义,带有一个watchdog,说明和一个看门狗的性质是一样的,超过一定的时候不喂狗,就会被狗咬,在Application的onCreate()中停掉该线程
    目前尝试采用滴滴Booster的解决方案

如何停掉一个线程

遵循原则:线程在终止的过程中,应该先进行操作来清除当前的任务,保持共享数据的一致性,然后再停止

  1. 使用stop方法强行终止,但是不推荐这个方法,因为stop一样都是过期作废的方法。
  2. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
    原则:一个可取消的任务要求必须设置取消策略,即如何取消,何时检查取消命令,以及接收到取消命令之后如何处理
  3. 使用interrupt方法中断线程。
    原则:由于每个线程拥有各自的中断策略,因此除非清楚中断对目标线程的含义,否者不要中断该线程。
    扩展:如何停掉一个kotlin协程,大家可以自行查阅
    看下滴滴是如何处理的 http://igit.58corp.com/com.wuba.wuxian.android.main/58ClientHybridLib/merge_requests/25296134/diffs

参考文章

FinalizerWatchdogDaemon.java

  1. https://cs.android.com/android/platform/superproject/+/master:libcore/libart/src/main/java/java/lang/Daemons.java;l=307?q=Daemons
  2. https://www.jianshu.com/p/1da91111b048
  3. https://blog.csdn.net/jamin0107/article/details/78793021
  4. https://www.jianshu.com/p/e398e450c597
  5. https://segmentfault.com/a/1190000019373275
  6. https://www.jianshu.com/p/613286f4245e

掀乱书页的风

2021/10/16  阅读:23  主题:兰青

作者介绍

掀乱书页的风