重载Finalize引发的内存泄露

重载Finalize引发的内存泄露

一些同学在开发JNI的时候,通常会使用finalize来做native内存的释放,而在频繁的列表滑动创建和回收持有native内存的Java对象(简称NativeBean,后同)后,我们去使用内存分析工具,比如zprofiler、mat等,分析一些oom的heap时,经常能看到 java.lang.ref.FinalizerReference占用很大数量的NativeBean,而其实你并没有这么多数据项,那么这里面的原因是什么呢?

1、FinalizerReference

通过MAT这些工具你会发现,我们的NativeBean都被FinalizerReference持有者,这个FinalizerReference主要目的就是为了协助FinalizerDaemon守护线程完成对象的finalize工作而生的,我们先从代码来看起。

public final class FinalizerReference<T> extends Reference<T> {
    // This queue contains those objects eligible for finalization. public static final ReferenceQueue queue = new ReferenceQueue();

   ..............
   ..............

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

这个类 提供了2个很重要的方法,add是将对象插入到ReferenceQueue中,而remove则是从中移出,而这个ReferenceQueue源码比较简单,大家可以自行去了解一下,其实可以简单理解是引用的队列,那么我们重点要了解清楚的是这些引用是什么时机添加到ReferenceQueue,什么时机又从中移出呢?

2、Add和Remove的时机

在我们了解时机之前,我们先学习一些基本概念,什么是finalizer类呢?其实我们知道类的修饰有很多,比如final,abstract,public等,如果某个类用final修饰,我们就说这个类是final类,上面列的都是语法层面我们可以显式指定的,在JVM里其实还会给类标记一些其他符号,比如finalizer,表示这个类是一个finalizer类(为了和java.lang.ref.Finalizer类区分,下文在提到的带finalizer标识的类时会简称为f类),GC在处理这种类的对象时要做一些特殊的处理,如在这个对象被回收之前会调用它的finalize方法。

也就是说重载了finalize函数,并且非空实现的类就是咱们所说的f类,接下来我们就聊聊add的时机。

了解LeakCanary原理的同学,对这个机制应该比较了解,当WeakReference 创建时,传入一个 ReferenceQueue 对象,当被 WeakReference 引用的对象的生命周期结束,一旦被 GC 检查到,GC 将会把该对象添加到 ReferenceQueue 中。那么对于FinalizerReference来说,他其实也是类似的,这个add方法是从虚拟机中反调回来的,当GC发生时queue中就会插入当前正准备释放内存的对象的f类引用。

那么f类又是在什么时机从ReferenceQueue中移出的呢?

public final class Daemons {

    ...
        private static class FinalizerDaemon extends Daemon {
        private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();
        private final ReferenceQueue<Object> queue = FinalizerReference.queue;
        private volatile Object finalizingObject;
        private volatile long finalizingStartedNanos;

        FinalizerDaemon() {
            super("FinalizerDaemon");
        }

        @Override public void run() {
            while (isRunning()) {
                // Take a reference, blocking until one is ready or the thread should stop try {
                    doFinalize((FinalizerReference<?>) queue.remove());
                } catch (InterruptedException ignored) {
                }
            }
        }

        @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")
        private void doFinalize(FinalizerReference<?> reference) {
            FinalizerReference.remove(reference);
            Object object = reference.get();
            reference.clear();
            try {
                finalizingStartedNanos = System.nanoTime();
                finalizingObject = object;
                synchronized (FinalizerWatchdogDaemon.INSTANCE) {
                    FinalizerWatchdogDaemon.INSTANCE.notify();
                }
                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是Daemons.java中定义的另一个守护线程,FinalizerReference中定义的queue的消费者就是它。它内部定义了一个ReferenceQueue类型的对象queue,并将其赋值为前面说的FinalizerReference中的定义的那个queue。run方法中通过ReferenceQueue的remove方法把保存在queue中的Reference获取出来并通过doFinalize方法来调用f类的finalize方法,这里我们就了解到了Remove时机,不过我这里还多说一点,说说一个我们遇到的finalize timed out异常的原理。

通过查看ReferenceQueue的源码可知,ReferenceQueue的remove方法是阻塞的,在队列中没有Reference时将阻塞直到有Reference入队。我们看一下doFinalize方法,通过从队列中获取出来的reference的get方法获取到被引用的真实对象,并在这里调用该对象的finalize方法。但在这之前会通过FinalizerWatchdogDaemon.INSTANCE.notify()唤醒FinalizerWatchdogDaemon守护线程,而这个FinalizerWatchdogDaemon它主要用来监控finalize方法执行的时长,并在finalize执行超时时会抛出,所以我们不要在finalize方法中做耗时操作。

3、f类的使用不当造成的影响

首先当然是我们要解决的内存泄露,由于Daemons中的几个都是守护线程,我们看到它会创建一个,这个线程的优先级并不是最高的,意味着在CPU很紧张的情况下其被调度的优先级可能会受到影响,所以当你在频繁创建f类对象时,他没有办法及时被回收,造成内存泄露。

比如当你在adapter中的getview去创建这个f类的时候,而当f类要被回收时他会首先加入到ReferenceQueue中,当你不断滑动列表去绘制,CPU资源紧张的情况下,这个守护线程没有被调度去消费这些存在ReferenceQueue中的f类,这样就有可能造成内存泄露。

与此同时由于第一次GC的时候会将对象加入到ReferenceQueue中来,导致f类的回收至少需要2次GC才能被回收,而守护线程的优先级低,很可能长时间没被回收,从而容易导致f类在资源紧张时进入到老年代,从而引起full gc造成卡顿。

4、解决策略

尽量不要重载finalize方法,而是通过自己业务的监控或者手动接口去释放内存,如果一定要使用,那么一定不要让这些频繁创建的对象,或者大对象通过finalize来释放,finalize最好是作为最后的保证。

如果一定要使用finalize方法,要记得调用super.finalize



参考文献:

infoq.com/cn/articles/j

bozhiyue.com/anroid/bok




PS:发这么篇水文的目的是下一篇内存优化要引用到,就先发出来了。。。

编辑于 2017-07-12 20:35