前期有一个内存泄露case跟多线程相关,简单记录如下:

问题描述

跑一晚上的内存测试后,会出现很多的内存泄露,泄露trace如下

In *********:2.0.0:2.
* ************.editor.photo.app.PhotoEditor has leaked:
* GC ROOT static java.lang.Daemons$FinalizerDaemon.INSTANCE
* references java.lang.Daemons$FinalizerDaemon.oldFinalizingObject
* references android.view.RenderNode.mOwningView
* references android.view.View.mContext
* leaks *************.editor.photo.app.PhotoEditor instance
* Retaining: 23 MB.
* Reference Key: 62e93630-d8b7-4fda-9522-9e295e16d739
* Device: Xiaomi Xiaomi MI 5s Plus natrium
* Android Version: 7.0 API: 24
* Durations: watch=5082ms, gc=164ms, heap dump=2349ms, analysis=37914ms

从上述trace看,泄露都可达到23MB,已经是非常恐怖的泄露问题

分析过程

初步分析

最初怀疑跟object的finalize执行相关,FinalizerDaemon是用来gc的daemon thread,怀疑dump的时候,这个线程正好引用了被监控对象
具体解释如下:
实现了object的finalize()的类在创建时会新建一个FinalizerReference,这个对象是强引用类型,封装了override finalize()的对象,下面直接叫原对象。原对象没有被其他对象引用时(FinalizeReference除外),执行GC不会马上被清除掉,而是放入一个静态链表中(ReferenceQueue),有一个守护线程专门去维护这个链表,如何维护呢?就是轮到该线程执行时就弹出里面的对象,执行它们的finalize(),对应的FinalizerReference对象在下次执行GC时就会被清理掉。
一个堆的FinalizerReference会组成一条双向链表,垃圾回收器应该会持有链表头(链表头在FinalizerReference中为一个静态成员)。
为什么会泄漏:
直接原因就是守护线程优先级比较低,运行的时间比较少。如果较短时间内创建较多的原对象,就会因为守护线程来不及弹出原对象而使FinalizerReference和原对象都得不到回收。无论怎样调用GC都没有用的,因为只要原对象没有被守护线程弹出执行其finalize()方法,FinalizerReference对象就不会被GC回收。
但此种情况我们不可认为其为泄露,进程中还有一个Daemon线程FinalizerWatchdogDaemon,如果拥有finalize的object超过10s(MIUI改成了60s)没有被回收,则发生crash,但leakcanary的watch时间在5秒左右,也就是说,FinalizerDaemon线程引用被监控对象,在系统中是正常现象~跟Leakcanary提了一个issue,https://github.com/square/leakcanary/issues/856

深度分析

但看了下leakcanary源码,发现FinalizerDaemon是用来gc的daemon thread,怀疑dump的时候,这个线程正好引用了被监控对象这种情况应该是不存在的,解释如下
按照我们的假设,FinalizerDaemon.INSTANCE的finalizingObject成员持有被监控对象的引用,很有可能FinalizerDaemon.INSTANCE作为GC ROOT,泄露路径基本为

* GC ROOT static FinalizerDaemon.INSTANCE* references java.lang.Daemons$FinalizerDaemon.finalizingObject* references ********(被监控对象)

但看了下GCTrigger,有一句:
System.runFinalization();

看了下执行流:

System.runFinalization->Runtime.runFinalization()->VMRuntime.runFinalization->FinalizerReference.finalizeAllEnqueued
// Alloate a new sentinel, this creates a FinalizerReference.
Sentinel sentinel;
// Keep looping until we safely enqueue our sentinel FinalizerReference.
// This is done to prevent races where the GC updates the pendingNext
// before we get the chance.
do {
sentinel = new Sentinel();
} while (!enqueueSentinelReference(sentinel));
sentinel.awaitFinalization(timeout);
// Alloate a new sentinel, this creates a FinalizerReference.
Sentinel sentinel;
// Keep looping until we safely enqueue our sentinel FinalizerReference.
// This is done to prevent races where the GC updates the pendingNext
// before we get the chance.
do {sentinel = new Sentinel();
} while (!enqueueSentinelReference(sentinel));
sentinel.awaitFinalization(timeout);

在FinalizerReference中会等待队列的元素执行完finalize方法,这样的话,我们假设的场景在Leakcanary工具运行的过程中是不存在的,于是把提过的issue关闭了,排除不是系统问题,也不是工具问题,而是我们的代码出了问题~
泄露真正原因
从深度分析中可以看出,object的finalize方法的执行不会被工具检测为内存泄露,即使出现泄露,泄露元素也应该是
* references java.lang.Daemons$FinalizerDaemon.finalizingObject,而不是* references java.lang.Daemons$FinalizerDaemon.oldFinalizingObject,慢慢的真正的凶手走入视野~

commit 1423bd1d0ad5aefa4a7b19c975ff5ab3a7ed9eff
Author: yuanhuihui <****@****.com>
Date:   Mon Feb 20 17:49:03 2017 +0800adjust finnalizer watchdog1. It's not foreground process, no need to throw exception when finalizer exceed 10s.2. add finnaliing Object checker, whether occur timeout with different finalizing Object.Change-Id: I57083da6ae6bd6c358b88e6933a70e86109be299Signed-off-by: ****** 

先说下我对于FinalizerDaemon.oldFinalizingObject引起的内存泄露的理解
1:为什么出现内存泄露
《1》大概流程:
系统的许多工具类实现了object的finalize方法,这些类在创建时会新建FinalizerRefernece,封装override finalize方法的对象,是个强引用关系,GC时会将FinalizerReference放入静态链表ReferenceQueue(双向链表),FinalizerDaemon线程维护此链表,当线程获得执行资源时,从队列中弹出里面的FinalizerReference对象,并执行封装的object的finalize方法,而FinalizerWatchdogDeamon线程会监控 FinalizerDaemon线程的执行object.finalize()方法的快慢问题,如果ReferenceQueue是空的,FinalizerWatchdogDeamon线程会睡去,直到ReferenceQueue不为空,FinalizeDaemon线程会去唤醒FinalizerWatchdogDeamon,告诉它有工作来了
《2》泄露出现的场景
FinalizerReference

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

FinalizerDaemon线程的执行过程是OK的,引用关系清理了之后a就可被GC了,但还有一个关键:FinalizerWatchdog线程的waitForFinalization方法中,会将oldFinalizingObject赋值
// MIUI ADD:
FinalizerDaemon.INSTANCE.oldFinalizingObject = FinalizerDaemon.INSTANCE.finalizingObject;
这是个多线程的问题,此时finalizingObject的值不确定,很可能为null,很可能为指向对象a的引用,如果finalizingObject为null,oldFinalizingObject不会持有任何对象的引用,不会出现内存泄露问题,如果finalizingObject为指向对象a的引用,这句话会导致a的引用计数再+1,即使FinalizerDaemon线程执行完了doFinalize方法,也无法被回收,因为oldFinalizingObject依然持有对象a的引用,此时内存泄露就出现了,但如果下一次赋值oldFinalizingObject = finalizingObject发生了,也会导致a的引用计数-1,此时a就可正常被GC掉,泄露又消失了。
因此出现内存泄露的时间窗口:FinalizerDaemon线程先赋值finalizingObject,然后FinalizerWatchdogDaemon线程的waitForFinalization再执行,而且oldFinalizingObject = finalizingObject为最后一次赋值,最后FinalizerDaemon的doFInalize方法再执行 finalizingObject = null; ,此时就会出现内存泄露
因此此内存泄露是个偶现问题,而且比较隐蔽,也不会像FC,ANR那样很容易被发现,引发的内存泄露很可能会在后面的操作中,又被释放了
2:解决方法
<1>觉引入oldFinalizingObject是为了更好的debug,发现finalize超时问题的细节,但这样带来了内存问题,如果此change只是为了debug用,建议revert掉
<2>如果不能revert,感觉可在FinalizerDaemon线程的run方法中,在让FinalizerWatchdogDaemon线程睡去前,把oldFinalizingObject置为null,这样即使oldFinalizingObject = finalizingObject为最后一次赋值,ReferenceQueue为空时,将oldFinalizingObject置为null,会使内存里的对象引用计数恢复正常,可被正常GC掉

progressCounter.lazySet(++localProgressCounter);
// Slow path; block.
FinalizerWatchdogDaemon.INSTANCE.goToSleep();
finalizingReference = (FinalizerReference<?>)queue.remove();
finalizingObject = finalizingReference.get();
progressCounter.set(++localProgressCounter);
FinalizerWatchdogDaemon.INSTANCE.wakeUp();

问题修复

经好袁老师沟通后,决定将此change revert掉修复此问题~问题解决~

Android内存优化(一)之FinalizerDaemon和FinalizerWatchDog多线程内存泄露案例相关推荐

  1. Android 系统性能优化(82)---Android性能优化:手把手带你全面实现内存优化

    Android性能优化:手把手带你全面实现内存优化 在 Android开发中,性能优化策略十分重要 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 示意图 1. 定义 优化处理 应用程序的内存 ...

  2. Android系统性能优化(44)---全面详细的内存优化指南

    前言 在 Android开发中,性能优化策略十分重要 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 1. 定义 优化处理 应用程序的内存使用.空间占用 2. 作用 避免因不正确使用内存 &a ...

  3. Android性能优化:手把手带你全面实现内存优化

    前言 在 Android开发中,性能优化策略十分重要 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 1. 定义 优化处理 应用程序的内存使用.空间占用 2. 作用 避免因不正确使用内存 &a ...

  4. Android内存优化(二)之Bitmap的内存申请与回收(Android N和O的对比)

    在Android O上大面积的爆了大量native Bitmap相关的泄漏问题,最大能达到几十MB,开始怀疑是出现了native内存泄漏问题,但经分析后发现是Android N和Android O在处 ...

  5. Android内存优化(三)避免可控的内存泄漏

    相关文章 Android性能优化系列 Java虚拟机系列 前言 内存泄漏向来都是内存优化的重点,它如同幽灵一般存于我们的应用当中,有时它不会现身,但一旦现身就会让你头疼不已.因此,如何避免.发现和解决 ...

  6. Android性能优化之利用强大的LeakCanary检测内存泄漏及解决办法

    本篇文章主要介绍了Android性能优化之利用LeakCanary检测内存泄漏及解决办法,有兴趣的同学可以了解一下. 目录 前言 什么是内存泄漏? 内存泄漏造成什么影响? 什么是LeakCanary? ...

  7. 【MDCC技术大咖秀】Android内存优化之OOM

    大神分析的很全面,所以就转过来保存一份,转自:http://www.csdn.net/article/2015-09-18/2825737/1 以下为正文: Android的内存优化是性能优化中很重要 ...

  8. Android系统性能优化(69)---含内存优化、布局优化

    Android性能优化:含内存优化.布局优化 前言 在 Android开发中,性能优化策略十分重要 因为其决定了应用程序的开发质量:可用性.流畅性.稳定性等,是提高用户留存率的关键 本文全面讲解性能优 ...

  9. Android 性能优化----(3)内存优化指南

    前言 在 Android开发中,性能优化策略十分重要 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 1. 定义 优化处理 应用程序的内存使用.空间占用 2. 作用 避免因不正确使用内存 &a ...

最新文章

  1. soj1201- 约数
  2. Spark Master启动源码分析
  3. 动态链接库dll,静态链接库lib, 导入库lib 转
  4. mysql网络安装教程_详细教程--MySQL的安装与配置
  5. 神策 2020 数据驱动用户大会:新愿景 + 新定位 + 新舰队正式亮相!
  6. MyBatis3 用log4j在控制台输出 SQL----亲测,真实可用
  7. python观察日志(part27)--数组及矩阵运算
  8. 数据分析之如何制作数据埋点文档(二)
  9. PCL 学习(2)——基本数据类型与点云数据拼接
  10. 视频rtmp协议简介
  11. 大数据学习笔记60:构建Spark机器学习系统
  12. 创作焦虑之下,红人大V怎么看微博?
  13. 程序员界改BUG“神”发明,学会10分钟搞定一个BUG
  14. 吾生也有涯,而知也无涯,以有涯随无涯,殆己
  15. 2020年最好用的几个PHP开发工具推荐
  16. 神武授权位置服务器,《神武3》X诸葛八卦村 多益网络第二个大型文创项目即将开启...
  17. 给视频智能配音怎么弄?一步一步让你学会配音操作
  18. 常见的计算机局域网络的拓扑结构是,局域网常见的拓扑结构有哪三种
  19. 如何快速批量修改文件名
  20. 如何查看电脑运行记录

热门文章

  1. Skia深入分析10——Skia库的性能与优化潜力
  2. laradock配置入门
  3. mysql 结构体的charset_MYSQL源码分析之结构体浅析
  4. 【愚公系列】2022年08月 微信小程序-icon图标详解
  5. 面部特征点检测(使用opencv+dlib)
  6. 2011年,低价智能手机哪个平台做主?
  7. python字典setdefault方法后接append()的理解
  8. 起薪2万的爬虫工程师,Python需要学到什么程度才能就业?
  9. Java设计原则——开闭原则
  10. 学生党毕业论文福利,参考文献的排版方法(利用bib文件的方式)