上一篇Android进阶——性能优化以内存管理机制和垃圾回收机制(六)简述了Java内存管理模型、内存分配、内存回收的机制的相关知识,相信对于内存溢出也有了稍深的了解和体会,这一篇将从检测、解决内存泄漏进行总结。java
1、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(虚引用)。android
软/弱引用技术能够用来实现高速缓冲器:首先定义一个HashMap,保存软引用对象。算法
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再来定义一个方法,保存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();app
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();ide
Thread.sleep(2_000);
//没有被回收 由于软引用 在内存不足 回收
System.out.println("weak:"+weakReference.get());
System.out.println("weak queue:"+queue.poll());函数
}
}
对于软引用和弱引用的选择,若是只是想避免OutOfMemory异常的发生,则可使用软引用。若是对于应用的性能更在乎,想尽快回收一些占用内存比较大的对象,则可使用弱引用。另外能够根据对象是否常用来判断选择软引用仍是弱引用。若是该对象可能会常用的,就尽可能用软引用。若是该对象不被使用的可能性更大些,就能够用弱引用。另外软/弱引用能够和一个引用队列(ReferenceQueue)联合使用,若是软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列能够得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。工具
2、内存泄漏的检测
内存泄漏的缘由很不少种,仅仅依靠开发人员的技术经验没法准肯定位到形成内存泄漏的罪魁祸首,况且有些内存发生在系统层或者第三方SDK中,幸亏咱们能够借助专业的工具来进行检测,在使用工具检测前,咱们能够借助自动化测试手段或者其余手段进行初步测试,从前面的文章咱们知道发生内存泄漏的时候,内存是会变大的,好比说在android中咱们执行一段代码进入了一个新的Activity,这时候咱们的内存使用确定比在前一个页面大,而在界面finish返回后,若是内存没有回落,那么颇有可能就是出现了内存泄漏。post
一、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的极限堆大小,这个就是堆的默认最大值
二、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文件以外必须结合到实际的代码中去体会。
三、使用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),至此就能够精肯定位完毕内存泄漏的,结合代码分析(结合代码分析也是体力活和技术活,须要耐心和细心)
四、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...
}
}
3、内存泄漏的常见情形及解决办法
一、 静态变量引发的内存泄漏
在java中静态变量的生命周期是在类加载时开始,类卸载时结束,Static成员做为GC Roots,若是一个对象被static声明,这个对象会一直存活直到程序进程中止。即在android中其生命周期是在进程启动时开始,进程死亡时结束。因此在程序的运行期间,若是进程没有被杀死,静态变量就会一直存在,不会被回收掉。那么静态变量强引用了某个Activity中变量,那么这个Activity就一样也不会被释放,即使是该Activity执行了onDestroy(不要将执行onDestroy和被回收划等号)。
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.二、非静态内部类默认持有外部类实例的强引用引发的内存泄漏
内部类(包含非静态内部类 和 匿名类) 都会默认持有外部类实例的强引用,所以能够随意访问外部类。但若是这个非静态内部类实例作了一些耗时的操做或者声明了一个静态类型的变量,就会形成外围对象不会被回收,从而致使内存泄漏。一般这类问题的解决思路有:
将内部类变成静态内部类
若是有强引用Activity中的属性,则将该属性的引用方式改成弱引用。
在业务容许的状况下,及时回收,好比当Activity执行onStop、onDestory时,结束这些耗时任务。
1.2.一、匿名内部线程执行耗时操做引发的内存泄漏
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.二、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(){}
//一、将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);
}
}
二、集合类中只执行添加操做,而没有对应的移除操做
集合类若是仅仅有添加元素的方法,而没有相应的删除机制,致使内存被占用。若是这个集合类是全局性的变量 (好比类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,极可能致使集合所占用的内存只增不减。好比ButterKnife中的LinkedHashmap就存在这个问题(但实际上是一种妥协,为了不建立重复的XXActivity$$ViewInjector对象)
三、资源未关闭引发的内存泄漏
当使用了IO资源、BraodcastReceiver、Cursor、Bitmap、自定义属性attr等资源时,当不须要使用时,须要及时释放掉,若没有释放,则会引发内存泄漏
四、注册和反注册没有成对使用引发的内存泄漏
好比说调用了View.getViewTreeObserver().addOnXXXListener ,而没有调用View.getViewTreeObserver().removeXXXListener。
五、无限循环动画没有及时中止引发的内存泄漏
在Activity中播放属性动画中的一类无限循环动画,没有在ondestory中中止动画,Activity会被动画持有而没法释放
六、某些Android 系统自身目前存在的Bug
6.一、输入法引发的内存泄漏
如上图所示启动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();
}
4、内存抖动及修复措施
从上篇文章Android进阶——性能优化以内存管理机制和垃圾回收机制(六)咱们得知在Android5.0以后默认采用ART模式,采用的垃圾收集器是使用标记–清除算法的CMS 收集器,同时这也是产生内存抖动的根本缘由。
一、内存抖动Memory Churn
内存抖动是指在短期内有大量的对象被建立或被回收的现象,致使频繁GC,而开发时因为不注意,频繁在循环里建立局部对象会致使大量对象在短期内被建立和回收,若是频繁程度不够严重的话,不会形成内存抖动;若是内存抖动的特别频繁,会致使短期内产生大量对象,须要大量内存,并且还频繁回收建立。总之,频繁GC会致使内存抖动。
如上图所示,发生内存抖动时候,表现出的状况就是上下起伏,相似心电图同样(正常的内存表现应该是平坦的)
二、内存抖动的检测
经过Alloctions Tracker就能够进行排查内存抖动的问题,在Android Studio中点击Memory Profiler中的红点录制一段时间的内存申请状况,再点击结束,而后获得如下图片,而后再参照内存泄漏的步骤使用Profiler结合本身的代码进行分析。
三、内存抖动的优化
尽可能避免在循环体或者频繁调用的函数内建立对象,应该把对象建立移到循环体外。总之就是尽可能避免频繁GC。
小结 性能优化之路,历来都不是一蹴而就的,准确地来讲也没有任何技巧这两篇也仅仅是分享了一些常规的步骤,懂得了一些背后的故事,可是在实际开发中须要耐心和细心结合本身的代码区逐步完成优化工做。