常见的内存问题主要分为三种:java
指在短期内有大量的对象被建立或者被回收的现象android
危害:gc频繁,gc时会发生stw,致使卡顿git
指程序在申请内存后,没法释放已申请的内存空间github
危害:一次内存泄漏彷佛不会有很大影响,但内存泄漏堆积后的后果就是内存溢出,致使崩溃。即便没发生内存溢出,占据较多内存后,会引起gc,致使卡顿数据库
指程序运行要用到的内存大于能提供的最大内存,程序运行不了的现象markdown
危害:程序没法运行app
AndroidStudio提供的内存分析工具,经常使用于直观判断是否有内存抖动和内存泄漏dom
如图所示,能够直观的看出来内存的波动,从而判断是否有内存问题ide
Mat能够用于深度分析内存抖动和内存泄漏工具
使用方法以下:
一、先用Memory Profiler的堆转储功能,记录下一段时间内的内存状况,存储为hprof文件。
二、使用hprof-conv工具,转换从Memory Profiler保存下来的hprof文件。由于Mat没法解析直接从Memory Profiler保存下来的文件,可是Android官方提供了转换工具hprof-conv。hprof-conv在安卓SDK中的platform-tools目录中。使用命令以下
hprof-conv 旧hprof文件 转换后的hprof文件
三、使用Mat解析转换后的hprof文件
点击下图该按钮打开hprof文件
四、Mat的基本功能
查看某个类引用相关
查看GC Root
在dominator_tree中,查找Activity->Path To GC Roots->with all references
在本案例中,结果以下:
结果出了系统类引用外,Activity被单例PluginManager给引用了,形成了泄漏,Activity没法释放
LeakCanary是一个自动化的内存泄漏分析工具
使用方法
一、增长依赖
compile 'com.squareup.leakcanary:leakcanary-android:1.6.1'
复制代码
二、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);
}
}
复制代码
三、操做app,若是有泄漏则会通知出如今手机通知栏中,打开后,可看到如下这种内存泄漏的界面
LeakCanary原理
一、RefWatcher.watch() 建立一个 KeyedWeakReference 到要被监控的对象。
二、而后在后台线程检查引用是否被清除,若是没有,调用GC。
三、若是引用仍是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
四、在另一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
五、得益于惟一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
六、HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并肯定是不是泄露。若是是的话,创建致使泄露的引用链。
七、引用链传递到 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)
}
}
复制代码
ResourceCanary是腾讯的APM中检测内存泄漏的模块,该模块实现和leakCanary十分接近,有两个点不一样:
一、leakCanary在二次判断前,执行gc,后线程挂起100秒,由于调用gc后,虚拟机不必定立刻执行gc,但这种判断方法存在误判的可能,可能确实没有gc。ResourceCanary改良了作法,在gc前,设定一个普通可回收的对象的弱引用,将这个引用做为哨兵,当有gc发生,则这个对象被回收,反之,则这个对象还存活。依靠这个对象判断虚拟机是否真正执行了gc。
二、leakCanary分析的是完整的hprof文件,而ResourceCanary是先dump完整的hprof文件,后裁剪。比leakCanary多了一步裁剪工做,减少了hprof文件大小
可是,不管是leakCanary仍是ResourceCanary都不适合线上直接使用,由于完整的hprof文件很大,可能高达几百兆,不适合线上执行。
Tailor是一个内存快照裁剪压缩工具,经过hook技术在c层write时裁剪文件,不用存完整的hprof文件,避免dump下来完整的hprof文件,可线上使用
该库的具体实现可查看:https://github.com/bytedance/tailor
StriceMode是谷歌官方出的运行时检测工具,主要包含虚拟机策略和线程策略,虚拟机策略能够检测Activity泄漏、Sql对象泄漏,某个类实例个数是否超出等,检测结果能够从log中打印
StrictMode.setVmPolicy(
new StrictMode.VmPolicy.Builder()
//检测Activity泄漏
.detectActivityLeaks()
//检测数据库对象泄漏
.detectLeakedSqlLiteObjects()
//检测某个类的实例个数
.setClassInstanceLimit(PluginManager.class, 1)
//检测结果在log中打印,也可选择若是泄漏就退出应用
.penaltyLog()
.build());
复制代码
如何发现:使用memory profiler 观察,内存呈锯齿状,忽高忽低
常见缘由:通常是程序频繁建立对象引发。或者是内存泄露致使了内存不足,再申请频繁引起gc致使
危害:频繁gc,致使卡顿
解决方案:一般可利用Memory Profiler观察内存抖动,查看内存分配较多,实例较多的部分,找出堆栈排查
定义: 内存中存在没有用的对象回收不了
危害: 致使可用内存逐渐减小,严重时可能致使内存溢出
解决: 经过memory profiler 初步排查,可用内存是否逐渐减小,若是存在这种现象则可能有内存泄露。
在as.中使用堆转储功能下载hprof文件,经过platform tools中的工具转换格式后用mat打开分析。
一般找activity,查看存在对象个数,超过一个的不正常,用mat查看它的gc roots引用,找到缘由处理
集合类添加元素后,在使用完后未及时清理集合,致使集合中的元素没法释放
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
//o对象已经没用了,可是objectList还存在,o没法释放
o = null;
}
复制代码
static Context mContext;
复制代码
若是Activity被静态变量引用,则Activity会没法释放,须要特别注意
单例这种状况咱们特别容易忽略,须要养成习惯避免泄漏
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
非静态内部类/匿名类会持有外部类的引用,而静态内部类不会
这种状况常常发生在使用Handler的状况时,Handler被Message引用,而消息能够延时触发,好比延时6秒触发。而在6秒内Activity已经销毁了。这时Handler还不能释放,而Handler是非静态内部类/匿名类状况下,则会引用Activity,这时会形成泄漏
本文介绍了三种常见的内存问题,6种内存分析和检测的工具,内存抖动、内存泄漏的常见处理方式,以及Android中多种常见的因为编码不规范形成内存泄漏的缘由。实际生产中,内存问题很是隐蔽难以发现,最好是配合线上线下处理,线下使用如leakCanary这种优秀工具检测,线上经过对重点模块内存,oom率,gc次数等进行回传,针对部分用户进行内存快照回传自动化分析监控等操做,来持续地以较低成本进行内存优化。