4、常见内存泄漏
这是一个老生常谈的一个问题了,但我还是先对Java中的内存泄漏做一个定义:
Java中的内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。
对于Java中的内存泄漏,我总结为三点:static、线程 和 系统(外部)资源申请。
对于JVM内部而言,它的垃圾回收机制真的做的非常不错,我总觉得Java的出现是程序员的一次体力解放,大家再也不用去关心那个让RD们睡不好觉的指针问题;而JVM内部的内存泄漏,追究起原因来,我觉得(瞎估的)90%是static静态变量引起的、10%是while(true)的线程造成的,大家想一下,我们平时发现的内存泄漏,是不是大都是注册了监听没释放,或者是声明了一个大对象的static为了方便传递数据,结果忘了置空;对于static这样的变量,我们声明时要非常小心,我的意见是,不是非常必要的情况下,能不用尽量不要用。
外部资源申请一般指的是打开一些文件、设备、数据库等,这些系统都会给我们提供一些开销,如果我们没能及时关闭掉这些设备,则会造成不必要的内存开销。
Android上的内存泄漏会更具体一些,有些也会很隐蔽,我们来具体分析一下,这也是每个Android程序员面试都会考到的一个题。
(1)activity泄漏
这是我们平时最关心的泄漏,因为Activity在Android的四大组件中持的有资源最多,一个Activity没回收,会导致它里面的无数个View都无法被回收到。
<1>在需要Context的地方传入Activity,导致被静态变量持有,这种情况大家应该碰到的很多。
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static synchronized AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}....
// 使用时
AppManager am = AppManager.getInstance(activity);
上面这种情况,activity就被静态变量instance持有了,除非appmanager这个单例释放了。
这种情况有两个解决办法:
1、AppManager am = AppManager.getInstance(activity.getApplicationContext()); // 不建议
2、private AppManager(Context context) {
this.context = context.getApplicationContext();
}
建议使用第二种方法,因为这是从根上进行的解决,而第一种方法是需要调用者来保证解决,这种方法很不靠谱,而且从严格意义上说,调用都使用的根本没错,他传进去的Activity就是一个Context。
<2>静态变量Activity
这是Activity使用的大忌。
class MyActivity extends Activity {
public static Activity mActivity;
protected void onCreate(...) {
....
mActivity = this;
}
}
这种写法一般都是程序员偷懒,想通过static的属性快速获取一个Activity的实例,但这种情况下一般使用的人不知道什么时候去把mActivity置空,很容易泄漏。
<3>Static view或static Drawable
class MyActivity extends Activity {
public static View mView;
public static Drawable mDrawable;
}
这个跟<2>有点相似,都是程序员偷懒干出来的事情,只要这个view是这个activity中创出来的或者用activit inflate进来的,那View中的context就是Activity,所以view在,Activity就不会被回收;Drawable这个实际上是Android 2.3及以前的一个Bug,我们看代码:
//View.setBackground方法:
publicvoidsetBackgroundDrawable(Drawable background) {
....
background.setCallback(this);
.......
}// Drawable中的setCallback方法
public final void setCallback(Callback cb) {
mCallback = cb;
}
<4>非static的内部类
直接看代码
非静态内部类,只要不在赋值给静态变量,在类的内部使用起来还是非常方便的,因为它可以直接调用外部类的变量和方法。但如果我们在类的外部去new 一个这样的对象,我们应该怎么写呢?
MainActivity.TestResource inner = (new MainActivity()).new TestResource();
从这里可以看出,一个非静态的内部类对象是必须依附一个外部类对象存在的,这个时候如果内部类对象被静态变量持有,或者被传出去注册在哪里,就会导致外部类,比如这里的Activity无法回收。
<5>匿名内部类,我们最容易忽略的泄漏
再看一种:
AppManager.getInstance(mContext).registerStateChangedListener(new AppStateChangedListener() {
....
});
上面一种是线程造成的泄漏,线程不停,资源不释放;后一个是单例造成的;这两种匿名内部类没有变量持有,基本是必泄漏的。
<6>Handler泄漏
还是先看代码
我们去看一下handler的源码找一下原因:
每个Message在放入looper里面时,都会为这个message指定一个target,而这个target就是Handler,如果这个handler是一个内部类,就会造成对应的外部类泄漏。
我们看一下这个问题在网上的解决办法:
思考两个问题:弱引用是解决内存泄漏的首先方法吗?这里使用的官方的方法会出什么问题?
(2)注册和反注册
平时往单例中注册一些监听,正常都要在适当的时候进行反注册,除非这个监听是要伴随着整个进程的生命周期,这个比较容易理解,也是静态持有导致。
另一种注册和反注册的情况是receiver,Receiver正常是注册到系统中了,那到底被谁持有了呢
关于loadedApk这个类是在ActivityThread中初始化的,具体它的作用,可以在网上查找一下。
对于注册到localBroadcastManager中的Receiver,就更简单了,因为这个receiver在app内部使用,所以它就是一个类似往单例中注册listener的形式,必须反注册的。
(3)资源对象没关闭造成的内存泄露
这种情况的内存泄漏,就是我们开始说的向系统申请资源后没释放的情况,常见的是流和数据库未关闭,对于这种情况的细节就不做代码分析了,我的理解是:
linux对于每个设备等都是以文件来对待的,所以不管是文件还是设备,在打开时,系统都会为它创建一定的buffer,这个buffer是要占用内存空间的,如果没有关闭对应的流,这个buffer空间是一直被占用的。
(4)Bitmap的recycle不调会导致泄漏吗?
这一项是打了问号的,即到底Bitmap会不会造成内存泄漏呢?我们来一点点分析:
对于Bitmap的recycle()方法需不需要调用,网上的说法一般是这样的:
那,如果在2.3及以下,不显式的调用recycle(),是不是就内存泄漏了呢?
我觉得不会,因为官方对recycle()的解释里面,从没说必须要调,只是推荐。
从这两份文档来看,官方的意思应该是,这是一个高级调用,平时是不需要显式调用的,gc回回收这部分内存的,但2.3及以下,如果你确认一个bitmap的确不用了,还是调一下recycle比较好。
有点把人搞糊涂了,一般大家都认为2.3以下bitmap内存是native的堆中,gc收集不到,所以会引发一些OOM,但文档里又说GC会收集这些内存,让我们不用担心,到底是怎么一会事儿呢?
我们还是看源码吧,看源码能解决我们所有的疑惑,每次去看源码时,总能想来来Linus的那句话,好像是"Talk is cheap. Show me the code.",还有"Read the Fucking Source Code"。
源码地址:
https://android.googlesource.com/platform/frameworks/base.git/+/7f9f99ea11051614a7727dfb9f9578b518e76e3c/graphics/java/android/graphics/Bitmap.java
https://android.googlesource.com/platform/frameworks/base/+/android-2.2.1_r2/core/jni/android/graphics/Bitmap.cpp
https://chromium.googlesource.com/chromium/src/+/ae2c20f398933a9e86c387dcc465ec0f71065ffc/skia/sgl/SkBitmap.cpp
到了这里,大家可以再回过头思考一个问题,为什么2.3及以下的bitmap内存也不会泄露,可大家还总是会说2.3的图片分配在native,容易造成OOM呢?
5、MAT分析内存泄漏
对于内存问题的分析,AndroidStudio也提供了dump工具,但功能与mat比起来还是要弱很多,所以我平时还是习惯使用MAT来进行分析。
打开 DDMS 工具,在左边 Devices 视图页面选中“Update Heap”图标,然后在右边切换到 Heap 视图,点击 Heap 视图中的“Cause GC”按钮,到此为止需检测的进程就可以被监视。
Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。
所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。
MAT分析hprof来定位内存泄露的原因所在
这是出现内存泄露后使用MAT进行问题定位的有效手段。
A)Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类:
注意:这里dump出来的hprof文件,要想直接查看,是需要在eclipse中安装mat插件的;这也带来一个问题,要想方便查看,是要打开eclipse的,但eclipse与androidstudio是不兼容的,打开了一个,另一个的adb就连不上,这块的确比较麻烦。
B)使用OQL,查询内存中的对象:
我们在查询内存泄漏时,一般优先是看Activity,它持有的内存是四大组件中最多的,也是我们平时最容易出现的内存泄漏,为了快速查找出这类的对象,我们可以使用OQL来写。
C)分析这些持有引用的对象的GC路径
D)逐个分析每个对象的GC路径是否正常
从这个路径可以看出是一个antiRadiationUtil工具类对象持有了MainActivity的引用导致MainActivity无法释放。此时就要进入代码分析此时antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context导致节目退出后MainActivity无法销毁,那一般都属于内存泄露了)。
E)其它的使用:分析持有此类对象引用的外部对象