内存泄漏弄个明白

若是你参加面试,面试官常常会问到你的一个问题多是:你在开发过程当中,有过排除内存泄漏的经验吗?对于一个合格的Android/C/Java开发老手,这个问题想必已经深刻你的心;如果一名新手或者一直对内存泄漏这个东西模模糊糊的工程师,你的答案可能让面试官并不满意,这里将从底到上对内存泄漏的缘由、排查方法和一些经验为你作一次完整的解剖。html

处理内存泄漏的问题是将软件作到极致的一个必须的步骤,尤为是那种将被用户高强度使用的软件。java

 

一个简单的C和Android的例子jquery


一个最简单的C的内存泄漏的例子:android

char *ptr1 = (char *)malloc(10); char *ptr2 = (char *)malloc(10); ptr2 = ptr1; free(ptr1)

这里最后发生了10个字节的内存泄漏,那么到底发生了什么?程序员

首先各自分配了两块10个字节的内存,分别用叫ptr1和ptr2的指针指向这两块内存(就像是java中的引用),而后呢让ptr2也指向一开始ptr1指向的那块内存(这时候ptr1和ptr2都指向了ptr1一开始指向的那个10个字节的内存),最后用free将ptr1指向的那块内存给释放了——>结果就是一开始ptr2指向的那块内存发生了泄漏(没人用了却又回收不掉)面试

 

可能你会说Java有内存垃圾回收机制,不要考虑谁分配和释放的访问,那么下面这个例子就会让你明白你错了:网络

复制代码
public class PendingOrderManager { private static PendingOrderManager instance; private Context mContext; public PendingOrderManager(Context context) { this.mContext = context; } public static PendingOrderManager getInstance(Context context) { if (instance == null) { instance = new PendingOrderManager(context); } return instance; }

   public void func(){
...
}
... }
复制代码

而后让你的某个Activity去使用这个PendingOrderManager单例,而且某个时候退出这个Activity:app

//belong to some Activity
PendingOrderManager.getInstance(this).func(); ... finish()

 

这个时候内存泄漏已经发生:你退出了你的这个Activity本觉得java的垃圾回收会将它释放,但实际上Activity一直被PendingOrderManager持有着。Acitivity这个Context被长生命周期个体(单例一旦被建立就是整个app的生命周期)持有致使了这个Context发生了内存泄漏。异步

这个例子和上面的例子是相通的,上面的C的例子由于忘记了手动执行free一个10字节内存致使内存泄漏。而下面这个例子是垃圾回收机制“故意忘记”了回收Context的内存而致使了内存泄漏。下面两节将对这个里面到底发生了什么进行说明。函数

 

静态、堆和栈


编译原理说软件内存分配的时候通常会放在三种位置:静态存储区域、堆和栈,他们的位置、功能、速度都各不相同,区别以下:

 

  • 静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序整个运行期间都存在。它主要存放静态数据、全局static数据和常量
  • 栈:就是CPU的寄存器(并非内存),特色是容量很小可是速度最快,函数或者方法的的方法体内声明的变量或者指向对象的引用、局部变量即分配在这里,生命周期到该函数或者方法体尾部即止
  • 堆:就是动态内存分配去(就是实体的内存RAM),C中malloc和fee,java中的new和垃圾回收直接操做的就是这里的区域,类的成员变量分配在这里

从上面便可看出静态存储区域是编译时已经分配好的,栈是CPU自动控制的,那么咱们所讨论的内存泄漏的问题实际上就是分配在堆里面的内存出现了问题,通常问题在于两点:

  1. 快速不断的进行new操做。好比Android的自定义View的时候你在onDraw里面new出对象,就会致使这个自定义View的绘制特别卡,这是由于onDraw是快速重复执行的方法,在这个方法里面每次都new出对象会致使接二连三的new出新的对象,也致使gc也在不断的执行从而不断的回收堆内存。因为堆位于内存RAM上,这样子就致使了内存的不断的分配和回收消耗了CPU,同时致使了内存出现“空洞”(由于堆内存不是连续的)
  2. 忘记释放。若是你忘记了手动释放应该释放的内存,或者gc误判致使没有释放本应该释放的内存,那么久致使了内存泄漏。因为Android给一个app可在堆上(能够在AndroidManifest设置一个largeHeap="true"增大可分配量)分配的内存量是有限的,若是内存泄漏不断的发生,总有一天会消耗完毕,从而致使OOM

 

Java有了垃圾回收(GC)为何任而后内存泄漏


在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不须要经过调用函数来释放内存,但它只能回收无用而且再也不被其它对象引用的那些对象所占用的空间。可是误判是常常发生的,有些内存实际上已经没有用处了,可是GC并不知道。这里简单介绍下GC的机制:

上面一节说过栈上的局部变量能够引用堆上的分配的内存,因此GC发生的时候,通常是遍历一下静态存储区、栈从而列出全部堆上被他们引用的内存(对象)集合,这些内存都是有个引用计数,那么除此以外,其余的内存就是没有被引用的(或者说引用计数归零),这些内存就是要被释放的,随后GC开始清理这些内存(对象)

 

那么这里第一节的两个例子就很好理解了,那个单例模式因为生命周期太长(能够把他看做一个虚拟的栈中的局部变量)而且一直引用了Context(即Activity),因此GC的时候发现这个Activity的引用计数仍是大于1,因此回收内存的时候把他跳过,但实际上咱们已经不须要这块内存了。这样就致使了内存泄漏。

 

Android使用弱引用和完美退出app的方法


从上面来看,内存泄漏由于对象被别人引用了而致使,java为了不这种问题(假如你的单例模式必需要传入个Context),特意提供了几个特殊引用类型,其中一个叫作弱引用WeakReference,当它引用一个对象的时候,即便该WeakReference的生命周期更长,可是只要发生GC,它就当即释放所被引用的内存而不会继续持有。

这里有一个经常使用的例子:

一般咱们会在自定义的Application中来记住app中建立的Activity,从而中途在某个Activity中须要彻底退出app时能够彻底的销毁全部已经打开的Activity,这里咱们能够对自定义Application改造,让其只有一个对Activity的弱引用的HashMap,大体的代码以下:

复制代码
public class CustomApplication extends Application { private HashMap> activityList = new HashMap>(); private static CustomApplication instance; public static CustomApplication getInstance() { return instance; } public void addActivity(Activity activity) { if (null != activity) { L.d("********* add Activity " + activity.getClass().getName()); activityList.put(activity.getClass().getName(), new WeakReference<>(activity)); } } public void removeActivity(Activity activity) { if (null != activity) { L.d("********* remove Activity " + activity.getClass().getName()); activityList.remove(activity.getClass().getName()); } } public void exit() { for (String key : activityList.keySet()) { WeakReference activity = activityList.get(key); if (activity != null && activity.get() != null) { L.d("********* Exit " + activity.get().getClass().getSimpleName()); activity.get().finish(); } } System.exit(0); android.os.Process.killProcess(android.os.Process.myPid()); } }
复制代码

 

咱们在自定义的Activity的基类BaseActivity中的onCreate执行:

CustomApplication.getInstance().addActivity(this);

在BaseActivity的onDestroy中执行:

CustomApplication.getInstance().removeActivity(this);

这样子自定义Application就能不泄露的持有全部打开的Activity的引用,同时当你须要中途退出app的时候,只须要执行:

//完美退出程序方法
CustomApplication.getInstance().exit();

 

哪些状况会致使内存泄漏


到此你应该对内存泄漏的本质已经有所了解了,这里列举出一些会致使内存泄漏的地方,能够做为排查内存泄漏的一个checklist

  • 某个集合类(List)被一个static变量引用,同时这个集合类没有删除本身内部的元素
  • 单例模式持有外部本应该被释放的对象(第一节中那个例子)
  • Android特殊组件或者类忘记释放,好比:BraodcastReceiver忘记解注册、Cursor忘记销毁、Socket忘记close、TypedArray忘记recycle、callback忘记remove。若是你本身定义了一个类,最好不要直接将一个Activity类型做为他的属性,若是必需要用,要么处理好释放的问题,要么使用弱引用
  • Handler。只要 Handler 发送的 Message 还没有被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。因为 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。所以这种实现方式通常很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易致使没法正确释放。如上所述,Handler 的使用要尤其当心,不然将很容易致使内存泄露的发生。
  • Thread。若是Thread的run方法一直在循环的执行不停,而该Thread又持有了外部变量,那么这个外部变量即发生内存泄漏。
  • 网络请求或者其余异步线程。以前Volley会有这样的一个问题,在Volley的response来到以前若是Activity已经退出了并且response里面含有Activity的成员变量,会致使该Activity发生内存泄漏,该问题一直没有找到合适的解决办法。不过看来Volley官网已经注意到这个问题了,目前最新的版本已经fix this leak

 

使用leakcanary


 

以前Android开发一般使用MAT内存分析工具来排查heap的问题,之类的文章比较多,你们能够本身找。这里推荐一个叫作leakcanary的工具,他能够集成在你的代码里面。这个东西你们能够参考:

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0509/2854.html

 

http://www.cnblogs.com/soaringEveryday/p/5035366.html

相关文章
相关标签/搜索