引言
上一篇Android进阶——性能优化之内存管理机制和垃圾回收机制(六)简述了Java内存管理模型、内存分配、内存回收的机制的相关知识,相信对于内存溢出也有了稍深的了解和体会,这一篇将从检测、解决内存泄漏进行总结,以下是性能优化系列的链接地址列表(持续更新):
- Android进阶——性能优化之APP启动时黑白屏的根源解析及对应的优化措施小结(一)
- Android进阶——性能优化之APP启动过程相关源码解析(二)
- Android进阶——性能优化之APP启动速度优化实战总结(三)
- Android进阶——性能优化之布局渲染原理和底层机制详解(四)
- Android进阶——性能优化之布局优化实战经验小结(五)
- Android进阶——性能优化之内存管理机制和垃圾采集回收机制(六)
- Android进阶——性能优化之内存泄漏和内存抖动的检测及优化措施总结(七)
- Android进阶——性能优化之进程提权与保活原理及手段完全解析(八)
- Android进阶——性能优化之进程提权与拉活原理及手段完全解析(九
- Android进阶——性能优化之一种更高效更轻量的序列化方案Protocol Buffer完全攻略(十)
一、Java的引用概述
通过A能调用并访问到B,那就说明A持有B的引用,或A就是B的引用。比如 Object obj = new Object();通过obj能操作Object对象,因此obj是Object的引用;假如obj是类Test中的一个成员变量,因此我们可以使用test.obj的方式来访问Object类对象的成员Test持有一个Object对象的引用。GC过程与对象的引用类型是密切相关的,Java1.2对引用的分类Strong reference(强引用), SoftReference(软引用), WeakReference(弱引用), PhatomReference(虚引用)。
引用名称 | 说明 | 生存时间 | 回收时机 |
---|---|---|---|
强引用 | 在程序代码中普遍存在的,比如”Object obj = new Object()”这种引用,只要强引用还在,垃圾收集器就不会回收被引用的对象。 | JVM停止运行时 | 从来不会 |
软引用 | 用来定义一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要内存溢出之前,会将这些对象列入回收范围进行第二次回收,如果回收后还是内存不足,才会抛出内存溢出,可以联合ReferenceQueue构造有效期、占内存大、生命周期出的对象的二级高速缓冲区 | 内存不足时终止 | 内存不足时 |
弱引用 | 用来描述非必须对象。但他的强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器回收时,无论内存是否足够,都会回收掉被弱引用关联的对象,可以联合ReferenceQueue构造有效期、占内存大、生命周期出的对象的一级高速缓冲器 | GC运行时终止 | 发生垃圾回收时 |
虚引用 | 也称为幽灵引用或者幻影引用,是最弱的引用关系。一个对象的虚引用根本不影响其生存时间,也不能通过虚引用获得一个对象实例。虚引用的唯一作用就是这个对象被GC时可以收到一条系统通知。 | GC运行时终止 | 发生垃圾回收时 |
软/弱引用技术可以用来实现高速缓冲器:首先定义一个HashMap,保存软引用对象。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
ReferenceQueue<Object> objectReferenceQueue = new ReferenceQueue<>();
SoftReference<Object> softReference = new SoftReference<>(softObj,objectReferenceQueue);//通过这个ReferenceQueue可以监听到GC回收
再来定义一个方法,保存Bitmap的软引用到HashMap。
public void static main(String args[]){
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
//软引用
Object softObj = new Object();
ReferenceQueue<Object> objectReferenceQueue = new ReferenceQueue<>();
SoftReference<Object> softReference = new SoftReference<>(softObj,objectReferenceQueue);//通过这个ReferenceQueue可以监听到GC回收
//引用队列
System.out.println("soft:"+softReference.get());
System.out.println("soft queue:"+objectReferenceQueue.poll());
//请求gc
softObj = null;
System.gc();
Thread.sleep(2_000);
//没有被回收 因为软引用 在内存不足 回收
System.out.println("soft:"+softReference.get());
System.out.println("soft queue:"+objectReferenceQueue.poll());
Object wakeObj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakReference = new WeakReference<>(wakeObj,queue);
//引用队列
System.out.println("weak:"+weakReference.get());
System.out.println("weak queue:"+queue.poll());
//请求gc
wakeObj = null;
System.gc();
Thread.sleep(2_000);
//没有被回收 因为软引用 在内存不足 回收
System.out.println("weak:"+weakReference.get());
System.out.println("weak queue:"+queue.poll());
}
}
对于软引用和弱引用的选择,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。另外软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。
二、内存泄漏的检测
内存泄漏的原因很很多种,仅仅依靠开发人员的技术经验无法准确定位到造成内存泄漏的罪魁祸首,何况有些内存发生在系统层或者第三方SDK中,幸好我们可以借助专业的工具来进行检测,在使用工具检测前,我们可以借助自动化测试手段或者其他手段进行初步测试,从前面的文章我们知道发生内存泄漏的时候,内存是会变大的,比如说在android中我们执行一段代码进入了一个新的Activity,这时候我们的内存使用肯定比在前一个页面大,而在界面finish返回后,如果内存没有回落,那么很有可能就是出现了内存泄漏。
1、OOM
通俗来说OOM就是申请的内存超过了Heap的最大值,OOM的产生不一定是一次申请的内存就超过了最大值,导致OOM的原因基本上都是因为我们的不良代码平时”积累”下来的。而Android应用的进程都是从一个叫做Zygote的进程fork出来的,Android 会对每个应用进行内存限制(通过ActivityManager实例的getMemoryClass()查看),也可以查看/system/build.prop中的对应字段来查看App的最大允许申请内存。
- -dalvik.vm.heapstartsize—— 堆分配的初始大小
- -dalvik.vm.heapgrowthlimit —— 正常情况下dvm heap的大小是不会超过dalvik.vm.heapgrowthlimit的值。
- -dalvik.vm.heapsize ——manifest中指定android:largeHeap为true的极限堆大小,这个就是堆的默认最大值
2、Android Studio 的Profiler初步定位内存泄漏可疑点
Profiler是Android Sutdio内置的一个检测内存泄漏的工具,使用Profiler第一步就是通过“Profiler app”运行APP
然后首先看到如下界面
点击Memory之后
-
强制执行垃圾收集事件的按钮。
-
捕获堆转储的按钮,用于捕获堆内存快照hprof文件。
-
记录内存分配的按钮,点击一次记录内存的创建情况再点击一次停止记录。
-
放大时间线的按钮。
-
跳转到实时内存数据的按钮。
-
事件时间线显示活动状态、用户输入事件和屏幕旋转事件。
-
内存使用时间表,其中包括以下内容:
• 每个内存类别使用多少内存的堆栈图,如左边的y轴和顶部的颜色键所示。 • 虚线表示已分配对象的数量,如右侧y轴所示。 • 每个垃圾收集事件的图标。
启动APP之后,我们在执行一些操作之后(一些可以初步判断内存泄漏的操作),然后开始捕获hprof文件,首先得先点击请求执行GC按钮——>点击Dump java heap按钮 捕获hprof日志文件稍等片刻即可成功捕获日志(当然一次dump可能并不能发现内存泄漏,可能每次我们dump的结果都不同,那么就需要多试几次,然后结合代码来排查),然后直接把这个XXX.hprof文件拖到Android Studio就可解析到如下信息:
通过上图可以得知内存中对象的个数(通常大于1就有可能是内存泄漏了需要结合自身的情况)、所占空间大小、引用组占的内存大小等基本信息,点击具体某个节点,比如说此处点击MainActivity下,选中某个任务然后点击自动分析任务按钮,还可以得到
通过Android Profiler可以初步定位到能内存泄漏的地方,不过这可能需要重复去测试捕获hprof文件,再去分析,不过性能优化永远不是一蹴而就的事情,也没有任何墨守成规的步骤,除了借助hprif文件之外必须结合到实际的代码中去体会。
3、使用Memory Analyzer Tool精确定位内存泄漏之处
在Android Studio 的Profiler 上发现为何会内存泄漏相对于MAT来说麻烦些,所以MAT更容易精确定位到内存泄漏的地方及原因,MAT 是基于Eclipse的一个检测内存泄漏的最专业的工具,也可以单独下载安装MAT,在使用MAT之前我们需要把Android Studio捕获的hprof文件转换一下,使用SDK路径下的platform-tools文件夹下hprof-conv 的工具就可以转成MAT 需要的格式。
//-z选项是为了排除不属于app的内存,比如Zygote
hprof-conv -z xxxx.hprof xxxx.hprof
执行上面那句简单的命令之后就可以得到MAT支持的格式,用MAT打开后
还可以切换为直方图显示形式(这里会显示所有对象的信息),假如说我们知道了可能是MainActivity引起的泄漏,这里可以直接通过搜索栏直接过滤(往往这也是在做内存泄漏检测比较难的地方,这需要耐心还有运气)
然后想选中的对象上右键选择
弹出的对话框还可以显示很多信息,这里不一一介绍,这里只使用“Merge Shortest Path GC Roots”这个功能可以显示出对象的引用链(因为发生内存泄漏是因为对象还是GC Roots可达,所以需要分析引用链),然后可以直接选择“exclude all phantom/weak/soft ect references ” 排除掉软弱虚引用,接着就可以看到完整的引用链(下层对象被上层引用)
- shallow heap——指的是某一个对象所占内存大小。
- retained heap——指的是一个对象与所包含对象所占内存的总大小。
- out查看这个对象持有的外部对象引用
- incoming查看这个对象被哪些外部对象引用
在分析引用链的时候也需要逐层去结合代码排查,这一步也是个体力活,比如说上例就是逐步排查之后定位到的是网易IM 的SDK一个叫做e的对象引用了(其中Xxx$Xx的写法代表的是Xxx中的一个内部类Xx),至此就可以精确定位完毕内存泄漏的,结合代码分析(结合代码分析也是体力活和技术活,需要耐心和细心)
4、LeakCanary
LeakCanary是Square开源一个检测内存泄漏的框架,使用起来很简单,只需要两步:
- 在build.gradle中引入库
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}
- 然后在Application中进行初始化即可,当可能导致内存泄漏的时候会自动提示对应的泄漏点
public class ExampleApplication 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);
// Normal app init code...
}
}
三、内存泄漏的常见情形及解决办法
1、 静态变量引起的内存泄漏
在java中静态变量的生命周期是在类加载时开始,类卸载时结束,Static成员作为GC Roots,如果一个对象被static声明,这个对象会一直存活直到程序进程停止。即在android中其生命周期是在进程启动时开始,进程死亡时结束。所以在程序的运行期间,如果进程没有被杀死,静态变量就会一直存在,不会被回收掉。那么静态变量强引用了某个Activity中变量,那么这个Activity就同样也不会被释放,即便是该Activity执行了onDestroy(不要将执行onDestroy和被回收划等号)。
1.1、单例模式需要持有上下文的引用的时,传入短生命周期的上下文对象,引起的Context内存泄漏
public class Singleton {
private Context mContext;
private volatile static Singleton mInstance;
public static Singleton getInstance(Context mContext) {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null)
mInstance = new Singleton(mContext);
}
}
return mInstance;
}
//当调用getInstance时,如果传入的context是Activity的context。只要这个单例没有被释放,这个Activity也不会被释放,就很可能导致内存泄漏
private Singleton(Context mContext) {
this.mContext = mContext;
}
}
解决这类问题的思路有二:寻找与该静态变量生命周期差不多的替代对象和将强引用方式改成弱(软)引用
public class Singleton {
private Context mContext;
private volatile static Singleton mInstance;
public static Singleton getInstance(Context mContext) {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null)
mInstance = new Singleton(mContext.getApplicationContext());//将传入的mContext转换成Application的context
}
}
return mInstance;
}
//当调用getInstance时,如果传入的context是Activity的context。只要这个单例没有被释放,这个Activity也不会被释放。
private Singleton(Context mContext) {
this.mContext = mContext;
}
}
Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下
1.2、非静态内部类默认持有外部类实例的强引用引起的内存泄漏
内部类(包含非静态内部类 和 匿名类) 都会默认持有外部类实例的强引用,因此可以随意访问外部类。但如果这个非静态内部类实例做了一些耗时的操作或者声明了一个静态类型的变量,就会造成外围对象不会被回收,从而导致内存泄漏。通常这类问题的解决思路有:
- 将内部类变成静态内部类
- 如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用。
- 在业务允许的情况下,及时回收,比如当Activity执行onStop、onDestory时,结束这些耗时任务。
1.2.1、匿名内部线程执行耗时操作引起的内存泄漏
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
test();
}
public void test() {
//匿名内部类会引用其外围实例MainActivity.this,所以会导致内存泄漏
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
改为静态内部类即可
public static void test() {
//静态内部类不会持有外部类实例的引用
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
1.2.2、Handler引起的内存泄漏
mHandler 为匿名内部类实例,会引用外围对象MainActivity .this,若该Handler在Activity退出时依然还有消息需要处理,那么这个Activity就不会被回收,尤其是延迟处理时mHandler.postDelayed更甚。
public class MainActivity extends Activity {
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
...
};
};
...
}
针对Handler引起的内存泄漏,可以把Handler改为静态内部类,对于外部Activity的引用改为弱引用方式,并且在相关生命周期方法中及时移除掉未处理的Message和回调
public class MainActivity extends Activity {
private void doOnHandleMessage(){}
//1、将Handler改成静态内部类。
private static class MyHandler extends Handler {
//2将需要引用Activity的地方,改成弱引用。
private WeakReference<MainActivity> mInstance;
public MyHandler(MainActivity activity) {
this.mInstance = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity activity = mInstance == null ? null : mInstance.get();
//如果Activity被释放回收了,则不处理这些消息
if (activity == null || activity.isFinishing()) {
return;
}
activity.doOnHandleMessage();
}
}
@Override
protected void onDestroy() {
//3在Activity退出的时候移除回调
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
}
2、集合类中只执行添加操作,而没有对应的移除操作
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如ButterKnife中的LinkedHashmap就存在这个问题(但其实是一种妥协,为了避免创建重复的XXActivity$$ViewInjector对象)
3、资源未关闭引起的内存泄漏
当使用了IO资源、BraodcastReceiver、Cursor、Bitmap、自定义属性attr等资源时,当不需要使用时,需要及时释放掉,若没有释放,则会引起内存泄漏
4、注册和反注册没有成对使用引起的内存泄漏
比如说调用了View.getViewTreeObserver().addOnXXXListener ,而没有调用View.getViewTreeObserver().removeXXXListener。
##5、无限循环动画没有及时停止引起的内存泄漏
在Activity中播放属性动画中的一类无限循环动画,没有在ondestory中停止动画,Activity会被动画持有而无法释放
6、某些Android 系统自身目前存在的Bug
6.1、输入法引起的内存泄漏
如上图所示启动Activity的时候InputMethodManager中的DecorView类型的变量mCurRootView/mServedView/mNextServedView会自动持有相应Activity实例的强引用,而InputMethodManager可以作为GC Root就有可能导致Activity没有被及时回收导致内存泄漏。
要处理这类问题,唯一的思路就是破坏其引用链即把对应的对象置为null即可,又由于不能直接访问到,只能通过反射来置为null。
InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
try {
Field mCurRootViewField = InputMethodManager.class.getDeclaredField("mCurRootView");
mCurRootViewField.setAccessible(true);
Object mCurRootView = mCurRootViewField.get(im);
if (null != mCurRootView){
Context context = ((View) mCurRootView).getContext();
if (context == this){
//置为null
mCurRootViewField.set(im,null);
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
四、避免内存泄漏(OOM)的一些措施
- 合理使用一些轻量级的数据结构,在Android中很多情况下,我们可以优先考虑使用SparseArray或者ArrayMap替代HashMap等传统Java的数据结构,相比起这些Android特意开发的ArrayMap、SparseArray的容器,传统容器消耗更多的资源和相对低效些。因为HashMap自身需要一个额外的实例对象来记录Mapping操作导致更加消耗内存,同时SparseArray避免了key和value的自动装箱,从而避免了自动拆箱。
- 合理使用Bitmap,尽量减小BitMap的内存占用。在把图片加载到内存之前,可以根据需求对原始图片进行缩放处理,避免加载过大的图片,另外解码时使用合适的编码格式。
- 尽量复用Bitmap对象、ViewHolder等对象
- 在使用上下文的时候,尽量优先使用Appcation的Context,而非Activity的Context,当然局部的Dialog必须使用Activity的Context。
- 使用软引用来避免Handler的泄漏
- 谨慎使用static和单例对象,因为两者的生命周期都是与App的生命周期一样,如果不是需要全生命周期存在就尽量避免过度使用单例和static。
- 注意原始WebView的泄漏(Android系统中一个失败的控件,不仅仅兼容性存在问题,泄漏也是一大败笔),如果一定使用原始WebView,最好在单独的进程中运行。
五、内存抖动及修复措施
从上篇文章Android进阶——性能优化之内存管理机制和垃圾回收机制(六)我们得知在Android5.0之后默认采用ART模式,采用的垃圾收集器是使用标记–清除算法的CMS 收集器,同时这也是产生内存抖动的根本原因。
1、内存抖动Memory Churn
内存抖动是指在短时间内有大量的对象被创建或被回收的现象,导致频繁GC,而开发时由于不注意,频繁在循环里创建局部对象会导致大量对象在短时间内被创建和回收,如果频繁程度不够严重的话,不会造成内存抖动;如果内存抖动的特别频繁,会导致短时间内产生大量对象,需要大量内存,而且还频繁回收创建。总之,频繁GC会导致内存抖动。
如上图所示,发生内存抖动时候,表现出的情况就是上下起伏,类似心电图一样(正常的内存表现应该是平坦的)
2、内存抖动的检测
通过Alloctions Tracker就可以进行排查内存抖动的问题,在Android Studio中点击Memory Profiler中的红点录制一段时间的内存申请情况,再点击结束,然后得到以下图片,然后再参照内存泄漏的步骤使用Profiler结合自己的代码进行分析。
3、内存抖动的优化
尽量避免在循环体或者频繁调用的函数内创建对象,应该把对象创建移到循环体外。总之就是尽量避免频繁GC。
小结
性能优化之路,从来都不是一蹴而就的,准确地来说也没有任何技巧这两篇也仅仅是分享了一些常规的步骤,懂得了一些背后的故事,但是在实际开发中需要耐心和细心结合自己的代码区逐步完成优化工作。