您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
Android中的内存泄露
 
  1596  次浏览      15
 2018-6-5
 
编辑推荐:
本文来自于csdn,本文主要从java的内存模型讲起,最终举出几个内存泄露的例子和解决方案。

java运行时内存模型

具体信息:http://gityuan.com/2016/01/09/java-memory/

回收算法

JVM回收算法主要有两种

1.引用计数法:每个对象有一个引用计数器,当对象被引用一次时计数器加一,引用失效计数器减一。当计数器为0时表示对象可以被回收。(由于无法解决相互引用问题而被废弃)

2.可达性算法:从GC ROOT节点开始遍历,可以连通的对象都是活对象。无法到达的对象可以被回收。

可以作为GC ROOT节点的对象

虚拟机栈的栈帧的局部变量表引用的对象

本地方法栈JNI引用的对象

方法区的静态变量和常量所引用的对象

例子

public class GCDemo {
public static void main(String[] args) {
GcObject obj1 = new GcObject();
GcObject obj2 = new GcObject();

obj1.instance = obj2;
obj2.instance = obj1;

obj1 = null;
obj2 = null;
}
}

class GcObject {
public Object instance = null;
}

引用计数法内存图

1.step1:GcObject实例1的引用计数+1,目前为1

2.step2:GcObject实例2的引用计数+1,目前为1

3.step3:GcObject实例2的引用计数+1,目前为2

4.step4:GcObject实例1的引用计数+1,目前为2

5.obj1 = null:GcObject实例1引用计数-1,目前为1

6.obj2 = null:GcObject实例2引用计数-1,目前为1

到此为止,GcObject实例1和2都无法使用,但是引用计数不为0,发生内存泄露。

可达性算法内存图

由可以被作为GC ROOT对象来看,虚拟机栈中obj1和obj2是两个GC ROOT起点,由于最终都将obj1与obj2设置为了null。因此GcObject1和2都无法可达,因此可以被回收。

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

Android中常见的内存泄露

Android中最常见的内存泄露是关于Activity的内存泄漏。其核心问题在于在Activity生命周期之外仍有其引用。从内存模型角度来讲,它在GC ROOTING时可达,但是它的onDestroy已经被执行。既它从我们期待的行为上来说应该被标识为可以被回收。但它仍然可达。以下就列出最常见的几种情况,请记住核心矛盾:Activity生命周期之外仍有其引用

Handler导致的内存泄漏

public class SampleActivity extends Activity {

private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// 延时10分钟发送一个消息
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { }
}, 60 * 10 * 1000);

// 返回前一个Activity
finish();
}
}

思考上面代码,它会发生严重的内存泄露。首先,mLeakyHandler被声明为了一个匿名内部类(自己思考,上述代码有几个匿名内部类),它隐式的持有了外部类Activity的强引用。之后,handler发出了一个10分钟的延时消息,接着Activity杀掉了自己。问题的关键在于handler发出的消息Message会在消息队列里存i在10分钟。它持有发出消息的handler的引用。而handler又持有Activity的强引用。这就导致Activity在其生命周期之外仍有强引用,发生了严重的内存泄露。

注意,问题的核心在于内部类以及匿名内部类都会隐式的持有外部类的强引用。这就导致一个类的生命不在由一个因素控制,变为了多个因素。在我们认为该结束的时候而没有产生正确的行为。解决的方案很简单,也很通用:

用静态内部类。

在静态内部类根据需求使用弱引用修饰需要引用的外部类资源。

public class SampleActivity extends Activity {
/**
* 匿名类的静态实例不会隐式持有他们外部类的引用
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() {
}
};

private final MyHandler mHandler = new MyHandler(this);

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// 延时10分钟发送一个消息.
mHandler.postDelayed(sRunnable, 60 * 10 * 1000);

// 返回前一个Activity
finish();
}

/**
* 静态内部类的实例不会隐式持有他们外部类的引用。
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;

public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}

@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();

if (activity != null) {
// ...
}
}
}
}

static变量导致的内存泄漏

static变量在进程启动的时候被分配内存空间。在进程被杀死的时候释放内存空间。因此,它的生命周期是贯穿整个应用的生命周期的。也就是说,当你的Activity被静态变量染指到。那么又会导致Activity在其生命周期之外仍有强引用,发生内存泄露。

常见的场景如下:

static Context
static View

两者的本质是一样的,因为View内部持有Context的引用。第一种最常见的就是单例模式的实现,将具体的Activity的Context传递进去构造单例对象,导致泄露。第二种是当View需要加载的资源比较大时,想一劳永逸。而产生内存泄露。

解决方法依然通用且简单,在Activity声明周期外使用Context尽量去使用Application的Context

Activiity内部声明的静态资源要及时释放, 在相应的声明周期方法中做好收尾工作

注册与解注册

在andorid开发中,我们经常会在Activity的onCreate中注册广播接受器、EventBus等,如果忘记成对的使用反注册,可能会引起内存泄漏。开发过程中应该养成良好的相关,在onCreate或onResume中注册,要记得相应的在onDestroy或onPause中反注册。

创建与关闭

在android中,资源性对象比如Cursor、File、Bitmap、视频等,系统都用了一些缓冲技术,在使用这些资源的时候,如果我们确保自己不再使用这些资源了,要及时关闭,否则可能引起内存泄漏。因为有些操作不仅仅只是涉及到Dalvik虚拟机,还涉及到底层C/C++等的内存管理,不能完全寄希望虚拟机帮我们完成内存管理。

在这些资源不使用的时候,记得调用相应的类似close()、destroy()、recycler()、release()等函数,这些函数往往会通过jni调用底层C/C++的相应函数,完成相关的内存释放。

Cursor对象及时关闭

IO对象及时关闭

集合造成的内存泄漏

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}

画个简单的内存图就可以看出在栈中分配的v指向了在堆中分配的十个空间,虽然每次置为null,但是将V作为GC ROOT的起点仍然可达每一个在堆中的Object对象,而我们本意是将每一个Object对象置为了null。

解决方案很简单:v = null 。本质上是断掉GC ROOT的起点。

   
1596 次浏览       15
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程