Android中内存泄漏超级精炼详解


一、前期基础知识储备

(1)什么是内存?

JAVA是在JVM所虚拟出的内存环境中运行的JVM的内存可分为三个区:堆(heap)、栈(stack)和方法区(method)。


(stack):是简单的数据结构,但在计算机中使用广泛。栈最显著的特征是:LIFO(Last In, First Out, 后进先出)。后来者居上。(跟线程中队列的顺序恰好相反)栈中只存放基本类型对象的引用(不是对象)。

(heap):堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。JVM只有一个堆区(heap),且被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身


方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量

(2)什么是内存泄漏?

提及内存泄露,就不得不提到内存溢出,这两个比较容易混淆的概念,我们来分析一下。

内存泄露:ML (Memory Leak),程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。

内存泄漏对程序的影响是:容易使得应用程序发生内存溢出,即 OOM


内存溢出:OOM(Out of Memory),程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现OOM。小明向他同桌小红要橡皮,周一要一块,用一半没还;周二要一块,用一半没还;周三要一块,用一半没还;周四周五也是一样,然后到下周一他就失去这个同桌了,并且被叫了家长。

内存溢出对程序的影响是:导致ANR错误甚至应用Crush。OOM错误最终会导致ANR错误,即应用程序无响应,经常无响应的应用程序用着是会有点“上火”,比如“炉石传说掌游宝”(捞钱倒是有一手,换头像10元起步),这个ANR错误真的是每次打开有会报错,有时候真的是十分的恼火。


二、Android内存泄漏原因分析

(1)内存泄漏原因分析

需掌握的概念:栈中只存放基本类型变量和对象的引用,栈(stack)可以自行清除不用的内存空间;栈中引用的对象的本体存放在堆中,但在JAVA中堆内存不会随着方法的结束而清空;所以如果我们不停的创建新对象,堆(heap)的内存空间就会被消耗尽;

聪明的Java引入了垃圾回收(garbage collection)去处理堆内存的回收,perfect;

但是实际情况是,仍然有很多情况导致内存泄漏:如果对象一直被引用无法被回收,造成内存的浪费,无法再被使用。所以对象无法被GC回收就是造成内存泄露的原因!

(2)Java垃圾回收机制分析

垃圾回收(garbage collection,简称GC)可以自动清空堆中不再使用的对象。在JAVA中对象是通过引用使用的。如果再没有引用指向该对象,那么该对象就无从处理或调用该对象,这样的对象称为不可到达(unreachable)。垃圾回收用于释放不可到达的对象所占据的内存。

回收机制分析:我们将栈定义为root,遍历栈中所有的对象的引用再遍历一遍堆中的对象。因为栈中的对象的引用执行完毕就删除,所以我们就可以通过栈中的对象的引用,查找到堆中没有被指向的对象,这些对象即为不可到达对象,对其进行垃圾回收。

但是:如果持有对象的强引用,垃圾回收器GC是无法在内存中回收这个对象

————————————————————我是重点分隔线——————————————————

(3)内存泄漏的真实原因

内存泄露的直接原因是:本该回收的对象,因为某些原因(对象的强引用被另外一个正在使用的对象所持有,且没有及时释放),进而造成内存单元一直被占用,浪费空间,甚至可能造成内存溢出!

内存泄漏的本质原因是:持有引用者的生命周期>被引用者的生命周期!


强引用:实际编码中最常见的一种引用类型。常见形式如:A a = new A();等。对于这类引用GC任何时候不会对其进行内存回收,在内存不足的情况下宁愿抛出Out of Memory(OOM内存溢出)。类似这样的都是强引用:

private final MessageReceived mMessageReceived=new MessageReceived(this);

————————————————————我是重点分隔线——————————————————

其实在Android中会造成内存泄露的情景无外乎两种

全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。

活在Activity生命周期之外的线程。没有清空对Activity的强引用。


小结:虽然现在手机内存在不停的提升,内存泄露兴许不会像dalvik时代由于虚拟机内存过小造成各种花样OOM。但是过量的内存泄露依然会造成内存溢出,影响用户体验,在如今定制系统层出不穷、机型花样越来越多的情况下解决好内存泄露的问题会让适配和稳定性进一步提高!

三、Android中什么导致内存泄漏

有开发者总结了如下一张表,这里供读者参考:


资源对象没关闭造成的内存泄漏——描述: 资源性对象比如(Cursor,File文件,SQLiteDatabase)往往都用了一些缓冲,我们在不使用的时候或者在使用完之后,应该及时关闭它们比如close()方法,以便它们的缓冲及时回收内存。

构造Adapter时,没有使用缓存的convertView——常见于使用ListView,如果,不使用convertView,那么每次列表加载都会加载一次对象,用户反复加载,就会造成大量的对象创建,得不到释放,极容易发生内存泄漏。

public View getView(int position, ViewconvertView, ViewGroup parent) {
View view = new Xxx(...);
... ...
return view;
}
修正示例代码:
public View getView(int position, ViewconvertView, ViewGroup parent) {
    View view = null;
        if (convertView != null) {
        view = convertView; 
        populate(view, getItem(position));
        ...
        } else {
                view = new Xxx(...);
                ...
    }
return view;
};

对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null ——有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的,视情况而定。

对于生命周期比Activity长的对象如果需要应该使用ApplicationContext ——这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免context相关的内存泄漏。最显著地一个是避免context逃出他自己的范围之外。使用Application context。这个context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一个context,记得使用application对象。你可以通过调用 Context.getApplicationContext() or Activity.getApplication()来获得。

集合中对象没清理造成的内存泄漏——我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。

使用Service之后,没有关闭服务——如果Service停止失败也会导致内存泄漏。

因为系统会倾向于把这个Service所依赖的进程进行保留,如果这个进程很耗内存,就会造成内存泄漏。建议使用IntentService,可以自动停止。

内存泄漏对我们来说并不是可见的,因为它是在堆中活动,而要想检测程序中是否有内存泄漏的产生,通常我们可以借助LeakCanary、MAT等工具来检测应用程序是否存在内存泄漏,当检测到程序中有内存泄漏的产生时,它将告诉我们该内存泄漏是由谁产生的和该内存泄漏导致谁泄漏了而不能回收,供我们复查。

最后 祝各位开发者/读者少bug少泄漏少溢出少crash!!!

猜你喜欢

转载自blog.csdn.net/weixin_41101173/article/details/79710782
今日推荐