一些同学在开发 JNI 的时候, 通常会使用 finalize 来做 native 内存的释放, 而在频繁的列表滑动创建和回收持有 native 内存的 Java 对象 (简称 NativeBean, 后同) 后, 我们去使用内存分析工具, 比如 zprofilermat 等, 分析一些 oom 的 heap 时, 经常能看到 java.lang.ref.FinalizerReference 占用很大数量的 NativeBean, 而其实你并没有这么多数据项, 那么这里面的原因是什么呢?
1FinalizerReference
通过 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, 什么时机又从中移出呢?
2Add 和 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 中的定义的那个 queuerun 方法中通过 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 方法中做耗时操作
3f 类的使用不当造成的影响
首先当然是我们要解决的内存泄露, 由于 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
参考文献:
- www.infoq.com/cn/articles
- www.bozhiyue.com/anroid/boke
来源: https://juejin.im/entry/5a8e6f2ef265da4e9f6fb534