一、常见内存问题
常见的内存问题主要分为三种:
1、内存抖动
指在短时间内有大量的对象被创建或者被回收的现象
危害:gc频繁,gc时会发生stw,导致卡顿
2、内存泄漏
指程序在申请内存后,无法释放已申请的内存空间
危害:一次内存泄漏似乎不会有很大影响,但内存泄漏堆积后的后果就是内存溢出,导致崩溃。即使没发生内存溢出,占据较多内存后,会引发gc,导致卡顿
3、内存溢出
指程序运行要用到的内存大于能提供的最大内存,程序运行不了的现象
危害:程序无法运行
二、常用的内存分析工具
1、Memory Profiler
AndroidStudio提供的内存分析工具,常用于直观判断是否有内存抖动和内存泄漏
如图所示,可以直观的看出来内存的波动,从而判断是否有内存问题
2、Mat
Mat可以用于深度分析内存抖动和内存泄漏
使用方法如下:
1、先用Memory Profiler的堆转储功能,记录下一段时间内的内存情况,存储为hprof文件。
2、使用hprof-conv工具,转换从Memory Profiler保存下来的hprof文件。因为Mat无法解析直接从Memory Profiler保存下来的文件,但是Android官方提供了转换工具hprof-conv。hprof-conv在安卓SDK中的platform-tools目录中。使用命令如下
hprof-conv 旧hprof文件 转换后的hprof文件
3、使用Mat解析转换后的hprof文件
点击下图该按钮打开hprof文件
4、Mat的基本功能
查看某个类引用相关
- with incoming references : 表示该类被 哪些外部对应 引用了
- with outgoing references : 表示 该类 持有了 哪些外部对象的引用
查看GC Root
在dominator_tree中,查找Activity->Path To GC Roots->with all references
在本案例中,结果如下:
结果出了系统类引用外,Activity被单例PluginManager给引用了,造成了泄漏,Activity无法释放
3、LeakCanary
LeakCanary是一个自动化的内存泄漏分析工具
使用方法
1、增加依赖
compile 'com.squareup.leakcanary:leakcanary-android:1.6.1'
2、Application中开启检测功能
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}
3、操作app,如果有泄漏则会通知出现在手机通知栏中,打开后,可看到以下这种内存泄漏的界面
LeakCanary原理
1、RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
2、然后在后台线程检查引用是否被清除,如果没有,调用GC。
3、如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
4、在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
5、得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
6、HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
7、引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
判断对象是否能被回收,是使用弱引用结合引用队列实现,当一个对象能被回收,则会被加入到关联的引用队列中。以此来判断对象是否泄漏
下面的代码是参照LeakCanary检测是否泄露的源码所写的一段检测代码,基本可以体现出leakCanary的泄露检测原理
class RefWatcher {
private val referenceQueue = ReferenceQueue<Any>()
private val retainedKeys = HashSet<String>()
private val gcTrigger = GcTrigger.Default
private val executor = Executors.newSingleThreadExecutor()
/**
* 在Activity destroy时,执行判断是否泄露
*/
fun watch(obj : Any) {
val key = UUID.randomUUID().toString()
retainedKeys.add(key)
val reference = KeyedWeakReference(key, obj, referenceQueue)
executor.execute {
//清除被加到队列中的对象
removeReachableReferences()
//第一次判断
if(gone(reference)) {
//没有泄露
} else {
//跑一遍gc,gc的同时会让线程挂起100ms
gcTrigger.runGc()
//再清除一次被加入ReferenceQueue中的引用
removeReachableReferences()
//再判断一次
if(gone(reference)) {
//没有泄露
} else{
//判断为泄露
}
}
}
}
/**
* 当一个对象可以被gc回收时,会被加入referenceQueue
* 被加入到referenceQueue的引用,则判定对象不会泄露,从retainsKey中移除
*/
private fun removeReachableReferences() {
var ref = referenceQueue.poll() as KeyedWeakReference
while(ref != null) {
retainedKeys.remove(ref.key)
ref = referenceQueue.poll() as KeyedWeakReference
}
}
/**
* retainedKeys中不包含的,则判定为可回收,不会泄露
*/
private fun gone(reference: KeyedWeakReference) : Boolean {
return !retainedKeys.contains(reference.key)
}
}
4、Matrix的ResourceCanary
ResourceCanary是腾讯的APM中检测内存泄漏的模块,该模块实现和leakCanary十分接近,有两个点不同:
1、leakCanary在二次判断前,执行gc,后线程挂起100秒,因为调用gc后,虚拟机不一定马上执行gc,但这种判断方法存在误判的可能,可能确实没有gc。ResourceCanary改良了做法,在gc前,设定一个普通可回收的对象的弱引用,将这个引用作为哨兵,当有gc发生,则这个对象被回收,反之,则这个对象还存活。依靠这个对象判断虚拟机是否真正执行了gc。
2、leakCanary分析的是完整的hprof文件,而ResourceCanary是先dump完整的hprof文件,后裁剪。比leakCanary多了一步裁剪工作,减小了hprof文件大小
但是,无论是leakCanary还是ResourceCanary都不适合线上直接使用,因为完整的hprof文件很大,可能高达几百兆,不适合线上执行。
5、字节跳动的Tailor
Tailor是一个内存快照裁剪压缩工具,通过hook技术在c层write时裁剪文件,不用存完整的hprof文件,避免dump下来完整的hprof文件,可线上使用
该库的具体实现可查看:https://github.com/bytedance/tailor
6、StrictMode
StriceMode是谷歌官方出的运行时检测工具,主要包含虚拟机策略和线程策略,虚拟机策略可以检测Activity泄漏、Sql对象泄漏,某个类实例个数是否超出等,检测结果可以从log中打印
StrictMode.setVmPolicy(
new StrictMode.VmPolicy.Builder()
//检测Activity泄漏
.detectActivityLeaks()
//检测数据库对象泄漏
.detectLeakedSqlLiteObjects()
//检测某个类的实例个数
.setClassInstanceLimit(PluginManager.class, 1)
//检测结果在log中打印,也可选择如果泄漏就退出应用
.penaltyLog()
.build());
三、内存抖动和内存泄漏
1、内存抖动
如何发现:使用memory profiler 观察,内存呈锯齿状,忽高忽低
常见原因:一般是程序频繁创建对象引起。或者是内存泄露导致了内存不足,再申请频繁引发gc导致
危害:频繁gc,导致卡顿
解决方案:通常可利用Memory Profiler观察内存抖动,查看内存分配较多,实例较多的部分,找出堆栈排查
2、内存泄漏
定义: 内存中存在没有用的对象回收不了
危害: 导致可用内存逐渐减少,严重时可能导致内存溢出
解决: 通过memory profiler 初步排查,可用内存是否逐渐减少,如果存在这种现象则可能有内存泄露。
在as.中使用堆转储功能下载hprof文件,通过platform tools中的工具转换格式后用mat打开分析。
通常找activity,查看存在对象个数,超过一个的不正常,用mat查看它的gc roots引用,找到原因处理
四、常见的内存泄漏原因
1、集合类
集合类添加元素后,在使用完后未及时清理集合,导致集合中的元素无法释放
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
//o对象已经没用了,但是objectList还存在,o无法释放
o = null;
}
2、静态变量
static Context mContext;
如果Activity被静态变量引用,则Activity会无法释放,需要特别注意
3、单例
单例这种情况我们特别容易忽略,需要养成习惯避免泄漏
public class SingleInstanceClass {
private static SingleInstanceClass instance;
private Context mContext;
private SingleInstanceClass(Context context) {
mContext = context;
}
public static SingleInstanceClass getInstance(Context context) {
if (instance == null) {
instance = new SingleInstanceClass(context);
}
return instance;
}
}
这种情况下,context如果传的是Activity就会泄漏,应该传Application的Context
4、非静态内部类/匿名类
非静态内部类/匿名类会持有外部类的引用,而静态内部类不会
这种情况经常发生在使用Handler的情况时,Handler被Message引用,而消息可以延时触发,比如延时6秒触发。而在6秒内Activity已经销毁了。这时Handler还不能释放,而Handler是非静态内部类/匿名类情况下,则会引用Activity,这时会造成泄漏
5、小结
本文介绍了三种常见的内存问题,6种内存分析和检测的工具,内存抖动、内存泄漏的常见处理方式,以及Android中多种常见的由于编码不规范造成内存泄漏的原因。实际生产中,内存问题非常隐蔽难以发现,最好是配合线上线下处理,线下使用如leakCanary这种优秀工具检测,线上通过对重点模块内存,oom率,gc次数等进行回传,针对部分用户进行内存快照回传自动化分析监控等操作,来持续地以较低成本进行内存优化。