记一次内存泄露优化过程

背景

项目目前存在使用久了或者重复打开关闭某个页面,内存会一直飙升,居高不下,频繁发生GC。静置一段时间后,情况有所改善,但是问题依旧明显,如图1-1、1-2。

图1-1.操作时的内存使用情况

图1-2.静置时的内存使用情况

如上图1-1,是通过Android Studio查看内存(灰色)和CPU(红色)使用情况,可以看出内存有发生抖动并且是处于比较高的状态,再者,从logcat可以看到一直发生GC,如下图1-3:

图1-3.

出现这些情况,是有很多因素造成的,最主要的原因是发生了内存泄露:页面关闭之后,该页面对象理应被回收,但由于某种原因导致它没有被及时回收掉,一直占据着内存,导致内存居高不下。

目标

本目标就是排查出应用内发生内存泄露的地方,并解决问题,避免组员踩重复的坑,减少应用内存泄露现象,减少内存抖动,减少OOM发生的几率,提高应用的性能、流畅度,减少卡顿。

内存泄露排查优化过程

内存泄露排查过程,有两种方法,一种是通过MAT手动分析排查,另一种是通过LeakCanary注入到应用内辅助排查。

使用MAT分析内存泄露

首先,需要下载、安装内存分析工具MAT(http://www.eclipse.org/mat/)。接着,需要dump取内存,并进行分析。打开应用,先进行几个简单的操作:进入首页->进入社区首页->进入一条问答详情界面->迅速关闭页面->再次进入问答详情界面->再关闭,以此重复几次。然后通过Android Studio的Android Monitor将此时的内存dump下来,dump下来的hprof文件,存放在项目目录下的captures文件夹下,如图1-4、1-5。

图1-4.dump内存

图1-5. hprof文件

dump下来的hprof文件还不能用MAT打开,还需要进一步处理。需要借助Android SDK的一个转换工具,如图1-6。

图1-6.hprof-conv转换工具

通过命令行执行命令:hprof-conv.exe ,是指源文件,是指目标文件,如下图1-7。

图1-7. hprof-conv命令

然后使用MAT打开转换后的1.hprof文件,如下图1-8。

图1-8.内存概览

上图显示的是,当前内存使用情况。打开红色箭头按钮,这是OQL,类似于SQL,可以通过查询语句,查询对象。

为了排查出哪些界面(Activity)发生了内存泄露,可以使用下面的OQL语句查询,如图1-9。

图1-9.

查询结果显示有HomeActivity、QaDetailActivity,是符合之前的操作(进入首页->进入问答详情,详情重复进出)。这里可以看出QaDetailActivity肯定是发生了内存泄露,因为查询结果出现了两个QaDetailActivity,说明内存中存在了两个QaDetailActivity对象,但是当QaDetailActivity被关闭时,是应该被回收的。接着,执行图1-10操作,进一步排查是什么导致了内存泄露。

图1-10.执行Path To GC Roots

图1-11.GC Roots 执行结果

由于JAVA的垃圾回收机制是,当一个对象被持有引用时,如果发生GC时,是不会被回收的。如图1-11所示,当除去弱引用和软引用,执行GC后,this$0(QaDetailActivity)对象还是被mErrorListener所引用,mErrorListener是项目中Volley网络请求库中的一个错误回调接口。可以看下,项目中代码是如何实现的,如图1-12。

图1-12.JsonGet的使用

图1-12是获取问答详情信息发起的JsonGet请求,而Response.Listener和Response.ErrorListener都是匿名对象,一个异步回调,回调之后处理相关逻辑。试想一下,当JsonGet发起请求后,或因网络阻塞,不能及时处理回调,这时候关闭了QaDetailActivity,但是由于Response回调未执行完,还持有QaDetailActivity对象,所以此时QaDetailActivity并不能被回收。所以,目前项目中所有使用该方式请求数据理论上都存在着内存泄露的风险。那么如何解决此问题引起的内存泄露呢?这个和Android常见的会引起内存泄露的Handler一样,都是没有适时的移除回调导致的。优化后的代码如图1-13。

图1-13.

就是将JsonGet等网络请求通过观察者模式进行封装, 在Activity onCreate addObserver,在onDestory时removeObserver,这样就可以避免上述所遇到的内存泄露问题了。

上面是一个完整的内存泄露排查过程,但是,你会发现,这是在一个黑箱检测,你不知道什么时候该去dump下内存进行分析,有可能你操作了很多界面之后dump下的内存并没有内存泄露问题。如果进行黑箱测试的话,这是一个耗时、耗力的过程,那么接下来介绍LeakCanary的使用,这是一个内存泄露检测神器。

使用LeakCanary检测内存泄露

LeakCanary是square开源的一个项目https://github.com/square/leakcanary,它是一个Android和Java的内存泄露检测库,可以大幅度减少了开发中遇到的OOM问题。在项目中,专门开了一个代码分支dev_LeakCanary,用来检测内存泄露。关于LeakCanary的工作原理,可以从github开源项目上的wiki中获知https://github.com/square/leakcanary/wiki/FAQ

  1. RefWatcher.watch()创建一个KeyedWeakReference去检测对象;
  2. 接着,在后台线程,它将会检查是否有引用在不是GC触发的情况下需要被清除的;
  3. 如果引用引用仍然没有被清除,将会转储堆到.hprof文件到系统文件中(it them dumps the heap into a .hprof file stored on the app file system.);
  4. HeapAnalyzerService是在一个分离的进程中开始的,HeapAnalyzer通过使用HAHA(https://github.com/square/haha )解析heap dump;
  5. 由于一个特殊的引用key和定位的泄露引用,HeapAnalyzer可以在heap dump中找到KeyedWeakReference;
  6. 如果有一个泄露,HeapAnalyzer计算到GC Roots的最短的强引用路径,然后创建造成泄露的引用链;
  7. 结果在app的进程中传回到DisplayLeakService,并展示泄露的通知消息;

那么如何使用它呢?

首先,添加相关依赖,如图1-14。

图1-14.LeakCanary依赖

然后在Application初始化,并获取一个RefWatcher对象,如图1-15。

图1-15.初始化LeakCanary

最后注册要监听的对象,比如,要监听Activity有没有内存泄露,那么,在BaseActivity$onDestory注册监听,如图1-16。

图1-16.监听Activity

这样所有继承BaseActivity都会被检测。同样进行开始的那样操作界面(进入首页->问答详情->重复进出)。这时候,会发现通知栏有消息,点开消息,如下图1-17。

图1-17.LeakCanary内存泄露分析界面

LeakCanary的分析结果显示,还是因为JsonGet的网络请求,异步回调引起的内存泄露,当然,如果要通过MAT查看更多信息,自己分析,可以在手机SD卡下Download/leakcanary文件夹下找到hprof文件,导出来之后根据之前的操作一样,先通过转换工具进行转换,再用MAT打开查看。

显而易见,通过LeakCanary进行内存泄露检测简单了不少,省时省力,但是如何解决内存泄露问题,还是要靠程序员自身的技能了,它只是告诉你哪里发生了内存泄露。

优化后对比

通过上面的内存泄露排查优化之后,使用相同包名(dev),相同环境(local环境下),相同租户和用户,尽可能的进行相同操作。

图1-18.优化后,操作时内存使用情况

与优化前图1-1相比,优化前内存的使用大小一直处于12M以上,优化后图1-18,内存使用在峰值在10M左右。静置一段时间后(系统发生GC,回收内存),如图1-19。

图1-19.优化后,静置时内存使用情况

静置后与图1-2相比,可以看出,两者内存差不多都稳定在9M左右,但优化后内存抖动情况明显减少。

说明:可能鉴于测试手段不足,不够严谨,测试结果存在偏差在所难免,但是,还是可以看出优化后的效果还是不错的。

总结

内存泄露,会导致应用在使用过程中,内存不断攀升,导致内存不足,应用不流畅,卡顿,甚至导致发生OOM,程序崩溃。所以,在开发过程中,应该避免书写一些会致使内存泄露的代码。这里总结一些常见的内存泄露情景,有则改之无则加勉。

1、 Handler。在使用Handler时应该通过传入一个runnable,来处理消息,然后在适当的时机移除该runnable,比如Activity onDestory时。

2、 网络请求异步回调。同Handler一样,应该在适当的时机移除异步回调操作,比如Activity onDestory时。

3、 匿名内部类。由于匿名内部类会隐式持有当前类的引用,所以应尽可能声明为static

4、 尽可能少使用静态成员变量。之前项目的一个bug就是这个情况引起的,当时的代码是这样的。

5、 在传递Context时,需要特别注意,如果可以的话,尽量使用Application Context。

时间: 2024-10-09 18:41:59

记一次内存泄露优化过程的相关文章

记一次内存泄露调试(memory leak)-Driver Monkey

Author:DriverMonkey Mail:[email protected] Phone:13410905075 QQ:196568501 硬件环境:AM335X 软件环境:linux 3.2 现象:1)系统运行一晚上,配置硬件操作失效 2)系统放置在那,没有用户输入会自己死机 调试过程: 第一步:分析硬件配置失效原因,怀疑配置硬件代码有问题 最后发现 代码 调用 system() 函数配置硬件没有调用成功 返回值 为 -1. 第二步: 继续上一步 分析 system() 在什么情况下会

Chrome V8系列--浅析Chrome V8引擎中的垃圾回收机制和内存泄露优化策略[转]

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制.因此,V8 将内存(堆)分为新生代和老生代两部分. 一.前言 V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带来的内存泄露问题. 但使用了垃圾回收即意味着程序员将无法掌控内存.ECMAScript没有暴露任何垃圾回收器的接口.我们无法强迫其进 行垃圾回收,更无法干预内存管理 内存管理问题:在浏览器中,Chrom

记一次内存泄露排查

最后在实现一个无限循环的ViewPager,展示图片,功能实现了,但是运行一段时间之后会挂掉. 多亏了AndroidStudio的Memory Monitor,发现了内存一直在增长. 怎么触发gc内存都不会减少,确定了内存泄露了,但是不知哪里出问题了. 一时想到的排查内存泄露的工具,就是MAT了,但是没找到AndroidStudio的MAT插件. 只能先把java heap dump出来先,如下图所示 dump出来之后,hprof文件会保存在项目下captures目录,之前一直不知到,找了很久.

android内存泄露优化总结

android手机给应用分配的内存通常是8兆左右,如果处理内存处理不当很容易造成OutOfMemoryError,我们的产品出现最多的错误也是OutOfMemoryError的异常, 在解决这个异常时在网上发现很多关于OutOfMemoryError的原因的介绍. OutOfMemoryError主要由以下几种情况造成: 1.数据库的cursor没有关闭.  操作Sqlite数据库时,Cursor是数据库表中每一行的集合,Cursor提供了很多方法,可以很方便的读取数据库中的值,     可以根

记一次mysql性能优化过程

摘要: 所谓mysql的优化,三分是配置的优化,七分是sql语句的优化,通过一些案例分析,希望给大家在工作中带来一些思路 由于配置是运行过那么长时间,很稳定,基本上不考虑,所以本次主要是sql的优化,并且集中在开源中国的个人空间.下面是这次优化的数据库版本: 案例一:粉丝查询优化 粉丝查询有2条sql --查询所有粉丝 SELECT user FROM osc_friends f INNER JOIN osc_users u ON u.id=f.user AND f.friend=? AND f

MEF引起的内存泄露

也许你编程的时候很小心,注意不引起内存泄露,例如不要被全局Static的变量引用上,注意Singleton的static引用,注意Event Handler注销,注意IDisposable接口实现,而且正确实现了IDisposable.但或许你还是有内存泄露,为何?因为你的IDisposable接口根本没有被触发!为什么?参考MSDN这个页面的”Dispose method not invoked ”章节.还有其它的内存泄露原因,比如第三方组件或框架,框架本身的内存泄露问题,已经框架本身有Lif

Java集群优化——必须了解的内存溢出与内存泄露

概念: 内存溢出 out of memory 是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory:比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出. 内存泄露 memory leak 是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光. 联系: memory leak会最终会导致out of memory,内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满

Java 内存泄露的理解与解决过程

本文详细地介绍了Java内存管理的原理,以及内存泄露产生的原因,同时提供了一些列解决Java内存泄露的方案,希望对各位Java开发者有所帮助. Java内存管理机制 在C++ 语言中,如果需要动态分配一块内存,程序员需要负责这块内存的整个生命周期.从申请分配.到使用.再到最后的释放.这样的过程非常灵活,但是却十分繁琐,程序员很容易由于疏忽而忘记释放内存,从而导致内存的泄露. Java 语言对内存管理做了自己的优化,这就是垃圾回收机制. Java 的几乎所有内存对象都是在堆内存上分配(基本数据类型

listView优化以及内存泄露问题

最经开发app使出现了由于ListView产生的内存泄露问题.我们知道内存泄露时很不好的.意味着,代码写的有点失败,需要做些优化改动. 经过这次的教训,以及在网上找了些资料,总结了一下,关于ListView的优化: listview优化问题: 首先,listview必须严格按照convertView及viewHolder格式书写,这样可以基本保证数据最优. 其次,如果自定义Item中有涉及到图片等等的,一定要做图片优化.bitmap释放可以不做. 第三,尽量避免在BaseAdapter中使用st