性能优化 | 记一次线上OOM问题处理

概述

最近线上监控发现 OOM 涨幅较大,因此去尝试定位和修复这个问题,在修复了一些内存泄漏和大对象占用问题后, OOM 依旧未达到正常标准,在这些新上报的 hprof 文件中,发现几乎所有 case 中都有个叫 FinalizerReference 的对象,数量巨多,内存占用高居首位,因此判断它就是引起本次 OOM 上涨的罪魁祸首。

ReferenceQueue

首先前置了解下 ReferenceQueue 引用队列是个啥,简言之就是用来存放 Reference 对象的队列,当 Reference 对象所引用的对象被 GC 回收时,该 Reference 对象就会被加入到引用队列 ReferenceQueue 中。即:

val queue = ReferenceQueue<Bean>()
val bean = Bean()
val reference = SoftReference(bean, queue)

上述代码中创建了一个 bean 强引用,以及一个 reference 软引用,当 bean 被回收时,该软引用 reference 对象会被放到 queue 队列中,后续该 reference 对象需要开发者自行处理(如从队列中 poll 等)。

Leakcanary 检测内存泄漏的原理就是应用的 ReferenceQueue 引用队列,以 Activity 为例:

  • 监听 Activity 的生命周期。
  • 在 onDestory 的时候,通过 ReferenceQueue 来创建对应 Actitity 的 Refrence 引用对象。
  • 一段时间后,从 ReferenceQueue 中读取,如果有这个 Actitity 的 Refrence,那么说明这个 Activity 的 Refrence 已经被回收;如果 ReferenceQueue 没有这个 Actitity 的 Refrence, 那就说明出现了内存泄漏。

关于 Reference 引用,网上一搜很多文章,大家估计比较清楚了,就不再赘述。以前做过一些相关的笔记,有兴趣可以看看 JVM垃圾回收-引用。

FinalizerReference

首先介绍一下 Finalizer 对象,它指的是在其 Java 类中复写了 finalize() 方法,且该方法非空的对象,有的地方称呼这种类叫 f 类,我们也这样叫它。在类加载过程中会把加载的 Java 类标记为是否 f 类。

FinalizerReference概述

现在开始看看 FinalizerReference 是个啥玩意,知己知彼,才能知道怎么去修它。这个 FinalizerReference 是用来协助 FinalizerDaemon 线程处理对象的 finalize() 工作的,一次性出现了三个名词,我们先从 FinalizerReference 看起。

看看它的部分代码:

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

    // This list contains a FinalizerReference for every finalizable object in the heap.
    // Objects in this list may or may not be eligible for finalization yet.
    private static FinalizerReference<?> head = null;

    // The links used to construct the list.
    private FinalizerReference<?> prev;
    private FinalizerReference<?> next;

    public static void add(Object referent) {
        FinalizerReference<?> reference = new FinalizerReference<Object>(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;
            }
        }
    }
}

FinalizerReference 中有两个静态方法: add 和 remove 方法。它创建了一个 FinalizerReference 类型的链表(表头用静态变量 FinalizerReference.head 表示),其中每个 FinalizerReference 对象都使用 ReferenceQueue 类型的静态变量 queue 来创建,这样当对应的对象Object referent被回收后,该 FinalizerReference 会被放入 ReferenceQueue 中。

FinalizerReference.add

上述 FinalizerReference.add 方法是虚拟机调用的,当创建对象的时候,如果发现该类是 f 类,就会调用 FinalizerReference.add 方法创建 FinalizerRefence 对象,并将其加入到 head 链表中。

// art/runtime/mirror/class-alloc-inl.h
template<bool kIsInstrumented, Class::AddFinalizer kAddFinalizer, bool kCheckAddFinalizer>
inline ObjPtr<Object> Class::Alloc(Thread* self, gc::AllocatorType allocator_type) {
  CheckObjectAlloc();
  gc::Heap* heap = Runtime::Current()->GetHeap();
  bool add_finalizer;
  switch (kAddFinalizer) {
    case Class::AddFinalizer::kUseClassTag:
      // 这里判断是不是 f 类
      add_finalizer = IsFinalizable();
      break;
    case Class::AddFinalizer::kNoAddFinalizer:
      add_finalizer = false;
      DCHECK_IMPLIES(kCheckAddFinalizer, !IsFinalizable());
      break;
  }
  // Note that the `this` pointer may be invalidated after the allocation.
  ObjPtr<Object> obj = heap->AllocObjectWithAllocator<kIsInstrumented, /*kCheckLargeObject=*/ false>(self, this, this->object_size_, allocator_type, VoidFunctor());
  if (add_finalizer && LIKELY(obj != nullptr)) {
    // 如果是 f 类,则调用 AddFinalizerReference 方法
    heap->AddFinalizerReference(self, &obj);
    // ...
  }
  return obj;
}

// art/runtime/gc/heap.cc
void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) {
  ScopedObjectAccess soa(self);
  ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));
  jvalue args[1];
  args[0].l = arg.get();
  InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
  // Restore object in case it gets moved.
  *object = soa.Decode<mirror::Object>(arg.get());
}

上面的 AddFinalizerReference 方法会调用到 Java 层的 FinalizerReference.add() 静态方法,就完成了 add 的操作。

FinalizerReference.remove

我们前面提到,当 Reference 对象所引用的对象被 GC 回收时,该 Reference 对象就会被加入到引用队列 ReferenceQueue 中,所以当 f 类的对象发生 gc 时,会将其对应的 FinalizerReference 对象加入到 FinalizerReference.queue 队列里。而 remove 的时机与 FinalizerDaemon 守护线程有关,我们看看这个线程的源码:

// libcore/libart/src/main/java/java/lang/Daemons.java
// private static abstract class Daemon implements Runnable
private static class FinalizerDaemon extends Daemon {
    private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();
    private final ReferenceQueue<Object> queue = FinalizerReference.queue;

    @Override public void runInternal() {
        // ...
        while (isRunning()) {
            try {
                // Use non-blocking poll to avoid FinalizerWatchdogDaemon communication when busy.
                FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
                // ...
                doFinalize(finalizingReference);
            } catch (InterruptedException ignored) {
            } catch (OutOfMemoryError ignored) {
            }
        }
    }
}

FinalizerDaemon.runInternal 方法中会通过 FinalizerReference.queue(ReferenceQueue引用队列) 的 poll/remove 方法拿到 queue 中的 Reference 引用,接着执行 doFinalize() 方法,该方法会调用 Finalizer 对象的 finalize() 方法:

private void doFinalize(FinalizerReference<?> reference) {
    FinalizerReference.remove(reference);
    Object object = reference.get();
    reference.clear();
    try {
        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;
    }
}

首先将获取到的 FinalizerReference 对象从 FinalizerReference.head 链表中移除,接着通过reference.get()方法得到 Java 对象,并执行其 finalize() 方法。

小结

FinalizerReference 的主要作用是协助 FinalizerDaemon 守护线程来执行 Finalizer 对象的 finalize() 方法。

  • 在 f 类创建对象时,调用 FinalizerReference.add() 方法创建一个 FinalizerReference 对象(使用 ReferenceQueue 队列)并加入到 head 链表里。
  • 在 f 类的对象被回收后,其对应的 FinalizerReference 对象会被加入到 ReferenceQueue 队列(FinalizerReference.queue)里。
  • FinalizerDaemon 守护线程从 FinalizerReference.queue 队列中取出 FinalizerReference 对象,并执行其对应 f 类对象的 finalize() 方法。

ReferenceQueueDaemon

上面看完了 FinalizerDaemon 守护线程,这里再看看 ReferenceQueueDaemon 守护线程,上面说到在创建引用对象 Reference 的时候可以关联一个 ReferenceQueue 队列,当被引用对象被 gc 回收时,该 reference 对象就会被加入到其创建时关联的队列去,这个加入队列的操作就是由 ReferenceQueueDaemon 守护线程完成的

private static class ReferenceQueueDaemon extends Daemon {
    @Override public void runInternal() {
        while (isRunning()) {
            Reference<?> list;
            try {
                synchronized (ReferenceQueue.class) {
                    if (ReferenceQueue.unenqueued == null) {
                        progressCounter.incrementAndGet();
                        do {
                            ReferenceQueue.class.wait();
                        } while (ReferenceQueue.unenqueued == null);
                        progressCounter.incrementAndGet();
                    }
                    list = ReferenceQueue.unenqueued;
                    ReferenceQueue.unenqueued = null;
                }
            } catch (InterruptedException e) {
                continue;
            } catch (OutOfMemoryError e) {
                continue;
            }
            ReferenceQueue.enqueuePending(list, progressCounter);
        }
    }
}

具体源码感兴趣的话可以自己去看看,这里就不贴太多源码了。

FinalizerWatchdogDaemon

这里再补充一下 FinalizerWatchdogDaemon 守护线程,它跟 FinalizerDaemon 以及 ReferenceQueueDaemon 线程是一起 start 的。我们再看一下上面 FinalizerDaemon 和 ReferenceQueueDaemon 线程的 runInternal() 方法:

// ReferenceQueueDaemon
@Override public void runInternal() {
    FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);
    while (isRunning()) {
        Reference<?> list;
        try {
            synchronized (ReferenceQueue.class) {
                if (ReferenceQueue.unenqueued == null) {
                    progressCounter.incrementAndGet();
                    FinalizerWatchdogDaemon.INSTANCE.monitoringNotNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);
                    do {
                        ReferenceQueue.class.wait();
                    } while (ReferenceQueue.unenqueued == null);
                    progressCounter.incrementAndGet();
                    FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.RQ_DAEMON);
                }
                list = ReferenceQueue.unenqueued;
                ReferenceQueue.unenqueued = null;
            }
        } catch (InterruptedException e) {
            continue;
        } catch (OutOfMemoryError e) {
            continue;
        }
        ReferenceQueue.enqueuePending(list, progressCounter);
        FinalizerWatchdogDaemon.INSTANCE.resetTimeouts();
    }
}

// FinalizerDaemon
@Override public void runInternal() {
    FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);
    while (isRunning()) {
        try {
            FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
            if (finalizingReference != null) {
                finalizingObject = finalizingReference.get();
            } else {
                finalizingObject = null;
                FinalizerWatchdogDaemon.INSTANCE.monitoringNotNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);
                finalizingReference = (FinalizerReference<?>)queue.remove();
                finalizingObject = finalizingReference.get();
                FinalizerWatchdogDaemon.INSTANCE.monitoringNeeded(FinalizerWatchdogDaemon.FINALIZER_DAEMON);
            }
            doFinalize(finalizingReference);
        } catch (InterruptedException ignored) {
        } catch (OutOfMemoryError ignored) {
        }
    }
}

上面的 monitoringNotNeeded 方法会休眠线程,停止 timeout 计时,而 monitoringNotNeeded 会唤醒 FinalizerWatchdogDaemon 守护线程。看看 FinalizerWatchdogDaemon 的源码,我加了一些注释:

private static class FinalizerWatchdogDaemon extends Daemon {
    // Single bit values to identify daemon to be watched.
    // 监控的两种类型
    static final int FINALIZER_DAEMON = 1;
    static final int RQ_DAEMON = 2;

    @Override public void runInternal() {
        while (isRunning()) {
            // sleepUntilNeeded 内部会调用 wait() 方法,不贴了
            if (!sleepUntilNeeded()) {
                continue;
            }
            final TimeoutException exception = waitForProgress();
            if (exception != null && !VMDebug.isDebuggerConnected()) {
                // 抛出异常 Thread.currentThread().dispatchUncaughtException(exception)
                timedOut(exception);
                break;
            }
        }
    }

    // 去掉 whichDaemon 的监控,进入 wait
    private synchronized void monitoringNotNeeded(int whichDaemon) {
        activeWatchees &= ~whichDaemon;
    }

    // 添加 whichDaemon 的监控, notify 线程
    private synchronized void monitoringNeeded(int whichDaemon) {
        int oldWatchees = activeWatchees;
        activeWatchees |= whichDaemon;
        if (oldWatchees == 0) {
            // 调用 notify 唤醒
            notify();
        }
    }

    // 判断是否添加了指定 whichDaemon 类型的监控
    private synchronized boolean isActive(int whichDaemon) {
        return (activeWatchees & whichDaemon) != 0;
    }

    // 调用 isActive 判断开启监控的类型,通过 Thread.sleep() 方法休眠指定时间
    // 休眠结束后返回 TimeoutException 异常,若途中监控的逻辑执行完成则表示未超时,返回 null
    // 等待时间: VMRuntime.getFinalizerTimeoutMs
    // If the FinalizerDaemon took essentially the whole time processing a single reference,
    // or the ReferenceQueueDaemon failed to make visible progress during that time, return an exception.
    private TimeoutException waitForProgress() {
        finalizerTimeoutNs = NANOS_PER_MILLI * VMRuntime.getRuntime().getFinalizerTimeoutMs();
        // ...
    }
}

上面一些细节代码就不贴出了, FinalizerWatchdogDaemon 主要用来监控两种类型的执行时长: FINALIZER_DAEMON 和 RQ_DAEMON。当执行超时时就会抛出 TimeOutException 异常,因此不要在 finalize() 方法中做耗时操作。

这个超时时间在 AOSP 中定义为 10s, 国内厂商可能会修改这个值, 据说有的改为了 60 s, 有兴趣的同学可以自己解包看看:

RUNTIME_OPTIONS_KEY (unsigned int,        FinalizerTimeoutMs,             10000u)

OOM排查

通过线上 hprof 文件排除了大对象和内存泄漏的可能,接着在 hprof 中发现存在大量的 X(用 X 表示业务上的某个对象) 对象堆积,这些对象对应的 Java 类是一个与 Native 层有关的类,其重写了 finalize() 方法,线下无法复现堆积大量 X 对象的路径。

可能有某个业务场景的代码逻辑使用不当造成了 X 对象的疯狂创建,导致 FinalizerDaemon 线程来不及回收

由于这个 X 对象的创建场景比较多,因此无法通过代码 review 来定位到有问题的创建代码,采取了一些监控手段后依旧无果。

用治标不治本的方式,显式调用系统 gc 和 runFinalization 方法

在定位不到具体的问题场景后,因为猜测是创建了大量的 X 对象导致 FinalizerDaemon 线程回收不及时,因此分别在主线程和子线程中显式调用系统 gc 和 runFinalization 方法来手动回收 X 对象。

结果:子线程调用无效果, OOM 未下降;主线程调用发生了 ANR。

查看 ANR 堆栈后,发现 ANR 的位置都在另外一个 f 对象的 finalize() 方法中调用到的一行 Native 代码,因此问题就明了了,是因为在某个 finalize() 方法中调用到的这行代码卡死了(逻辑问题导致死锁),因此阻塞了 FinalizerDaemon 线程的执行,进而导致对象堆积。另外本应发生的 TimeoutException 异常被我们的异常处理框架捕获了,因此未曾暴露出来。

总结

Java 中并没有析构函数, finalize() 实现了类似析构函数的概念,可以在对象被回收前做一些回收性的操作,在 JNI 开发中可能被用来进行 Native 内存的释放。关于 f 类使用不当会造成的影响:

  • FinalizerDaemon 守护线程的优先级不高,在 CPU 紧张的情况下调度可能会被影响,所以可能无法及时回收 f 类对象。
  • finalize 对象由于 finalize() 的引用,它变成了一个强引用,即使没有其他显式的强引用了,它也还是无法立即被回收。
  • 因此 f 对象至少需要 2 次 gc 才可能被回收,第一次 gc 时会将 f 对象加入到 ReferenceQueue 中,而等到 finalize() 方法执行完毕后的下一次 gc 里才有可能回收它。由于守护线程的优先级低,这期间可能已经发生过多次 gc 了,长时间未回收的话又可能会导致 f 类在资源紧张时进入到老年代,从而引起老年代的 gc 甚至是 full gc。

因此尽量不要重载 finalize() 方法,而是通过一些逻辑上的接口去释放内存,如果一定要重载它的话,也不要让一些需要频繁创建的对象或者大对象去通过 finalize() 释放,不然总有一天会出现问题的。

与此相关的守护线程有四个,感兴趣的同学可以深入看看相关源码:

// libcore/libart/src/main/java/java/lang/Daemons.java
private static final Daemon[] DAEMONS = new Daemon[] {
        HeapTaskDaemon.INSTANCE,
        ReferenceQueueDaemon.INSTANCE,
        FinalizerDaemon.INSTANCE,
        FinalizerWatchdogDaemon.INSTANCE,
};

作者:苍耳叔叔
链接:https://juejin.cn/post/7136063993537363975

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,可扫描下方文末CSDN官方认证卡片免费领取↓↓↓

请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值