您的位置:首页 > 移动开发 > Android开发

java/Android内存泄漏和内存溢出详解

2017-12-26 16:57 274 查看

java/Android内存泄漏和内存溢出详解

java内存泄漏和溢出跟内存栈堆也是有一些关系,这里不解释!

这里主要讲解一下内存泄漏和溢出的区别和联系。

之前我跟别人说这两个的区别就说了:内存泄漏是因为内存对象一直被占有没有释放,而内存泄漏严重就会导致内存溢出。

其实上面这个解释听起来有一些道理,但是不怎么正确,下面跟大家在详细解释一下:

一.内存泄漏和内存溢出的定义

1.内存泄露 memory leak

指程序在申请内存后,被某个对象一直持有,无法释放已申请的内存空间

一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

2.内存溢出 out of memory

指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

二.内存溢出的示例

内存溢出,是指分配的内存不够用!

自己申请的内存是一个方面,但是每个程序也是有固定大小的内存,有些程序可以申请扩大,但是都是有范围的,并且不会很大,一般超过这个范围就会造成内存溢出。

1.堆内存溢出示例代码

/**

* 堆溢出

*/

public class Heap

{

public static void main(String[] args)

{

ArrayList list=new ArrayList();

while(true)

{

list.add(new Heap());

}

}

}


上面无限的向堆创建内存空间,并且这个对象都是被集合对象持有,不能被释放,某个时刻肯定会造成内存溢出。

报错:

java.lang.OutOfMemoryError: Java heap space

上面heap就是堆的意思。

2.栈内存溢出示例代码

/**

* 栈溢出

*/

public class Stack

{

public static void main(String[] args)

{

new Stack().test();

}

public void test()

{

test();

}

}


这里是陷入无限的方法循环!

报错:

java.lang.StackOverflowError

statck就是栈的意思

上面就是内存溢出的示例,内存溢出还是比较好预防的,只要逻辑正确就可以杜绝,但是内存泄漏,有时候怎么都不能杜绝完全。

三.内存泄漏的分类:以发生的方式来分类,内存泄漏可以分为4类:

1. 常发性内存泄漏。

发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

2. 偶发性内存泄漏。

发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

3. 一次性内存泄漏。

发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

4. 隐式内存泄漏。

程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

四.内存泄漏/溢出能不能try catch处理

这个问题,大部分人应该是没有想过吧。

正常来说,内存泄漏try catch肯定是没有用的,但是内存溢出try catch程序还会不会报错呢!

我也是亲自测试了,比如用上面的堆内存泄漏的代码:

public class Heap {
public static void main(String[] args) {
try {
ArrayList list = new ArrayList();
while (true) {
list.add(new Heap());
}
} catch (Exception e) {
System.out.println("eee:" + e.getMessage());
}finally {
System.out.println("finally" );
}
}
}
}


后台打印数据:

finally

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

测试说明try catch对堆内存溢出是没有作用的!

栈内存溢出也是一样不能try catch。

五.内存泄漏和内存溢出后果

内存泄漏,会造成程序内存变大,因为有一些无用的内存,也可能导致程序比价卡!

内存溢出,那就麻烦了,程序直接崩溃,try catch都不能预防!

中大型的应用程序,100%是有内存泄漏的,只是多少的问题我们做的小程序,涉及到线程、Handler、自定义View比较多的时候也是有内存泄漏的。

频繁或者严重的内存泄漏也会造成内存溢出。

像我之前的启动页面,涉及到自定义View和线程倒计时设计,每次这个页面都会又1M左右的内存泄漏!怎么都解决不了,除非不用那些线程。

六.内存泄漏的原因和预防

内存溢出的预防上面也说了只要是逻辑完善一下就能预防。但是内存溢出就有一些技巧了。

1.集合类泄漏

先看一段代码的示例

List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
o = null;
}


上面的实例,虽然在循环中把引用o释放了,但是它被添加到了objectList中,所以objectList也持有对象的引用,此时该对象是无法被GC的。因此对象如果添加到集合中,还必须从中删除,最简单的方法

防止集合类泄漏内存的方法

置空集合对象即可

//释放objectList
objectList.clear();
objectList=null;


2.单例造成的内存泄漏,上下文范围

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子。

public class SingleInstanceClass {
private static SingleInstanceClass instance;
private Context mContext;

//在构造方法中传入上下文
private SingleInstanceClass(Context context) {
this.mContext = context;
}

public SingleInstanceClass getInstance(Context context) {
if (instance == null) {
instance = new SingleInstanceClass(context);
}
return instance;
}
}


正如前面所说,静态变量的生命周期等同于应用的生命周期,此处传入的Context参数便是祸端。如果传递进去的是Activity或者Fragment,由于单例一直持有它们的引用,即便Activity或者Fragment销毁了,也不会回收其内存。特别是一些庞大的Activity非常容易导致OOM。

上下文使用预防

正确的写法应该是传递Application的Context,因为Application的生命周期就是整个应用的生命周期,所以没有任何的问题。

private SingleInstanceClass(Context context) {
this.mContext = context.getApplicationContext();// 使用Application 的context
}


3.匿名内部类/非静态内部类和异步线程

非静态内部类创建静态实例造成的内存泄漏

我们都知道非静态内部类是默认持有外部类的引用的,如果在内部类中定义单例实例,会导致外部类无法释放。如下面代码:

public class TestActivity extends AppCompatActivity {
public static InnerClass innerClass = null;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (innerClass == null)
innerClass = new InnerClass();
}
private class InnerClass {
//...
}
}


当TestActivity销毁时,因为innerClass生命周期等同于应用生命周期,但是它又持有TestActivity的引用,因此导致内存泄漏。

正确做法应将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:



上面那么多关联,其实大部分是不怎么要知道的,你只要知道三种情况不能用Application就可以了:

第一种情况:对话框创建的上下文

第二种情况:跳转到其他Activity的上下文

第三种情况:创建布局Layout或View要用到的上下文

4.匿名内部类,线程

android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露。如下代码:

public class TestActivity extends AppCompatActivity {
//内部类的创建....

private Runnable runnable=new Runnable() {
@Override
public void run() {

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


上面的runnable所引用的匿名内部类持有TestActivity的引用,当将其传入异步线程中,线程与Activity生命周期不一致就会导致内存泄漏。

5.Handler造成的内存泄漏

Handler造成内存泄漏的根本原因是因为,Handler的生命周期与Activity或者View的生命周期不一致。Handler属于TLS(Thread Local Storage)生命周期同应用周期一样。看下面的代码:

public class TestActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void dispatchMessage(Message msg) {
super.dispatchMessage(msg);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
//do your things
}
}, 60 * 1000 * 10);
finish();
}
}


在该TestActivity中声明了一个延迟10分钟执行的消息 Message,mHandler将其 push 进了消息队列 MessageQueue 里。当该 Activity 被finish()掉时,延迟执行任务的Message 还会继续存在于主线程中,它持有该 Activity 的Handler引用,所以此时 finish()掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指TestActivity)。

修复方法:采用内部静态类以及弱引用方案。代码如下:

public class TestActivity extends AppCompatActivity {
private MyHandler mHandler;
private static class MyHandler extends Handler {
private final WeakReference<TestActivity> mActivity;
public MyHandler(TestActivity activity) {
mActivity = new WeakReference<>(activity);
}

@Override
public void dispatchMessage(Message msg) {
super.dispatchMessage(msg);
TestActivity activity = mActivity.get();
//do your things
}
}

private static final Runnable mRunnable = new Runnable() {
@Override
public void run() {
//do your things
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new MyHandler(this);
mHandler.postAtTime(mRunnable, 1000 * 60 * 10);
finish();
}
}


需要注意的是:使用静态内部类 + WeakReference 这种方式,每次使用前注意判空。

前面提到了 WeakReference,所以这里就简单的说一下 Java 对象的几种引用类型。

Java对引用的分类有 Strong reference, SoftReference, WeakReference, PhatomReference 四种。

如图:



前面所说的,创建一个静态Handler内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,但是这样做虽然避免了Activity泄漏,不过Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息。

下面几个方法都可以移除 Message:

public final void removeCallbacks(Runnable r);
public final void removeCallbacks(Runnable r, Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public final void removeMessages(int what, Object object);


6.尽量避免使用 staic 成员变量

如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。

这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,意味着如果此app做过进程互保保活,那会造成app在后台频繁重启。就会出现一夜时间手机被消耗空了电量、流量,这样只会被用户弃用。

这里修复的方法是:

不要在类初始时初始化静态成员。可以考虑lazy初始化。

架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

避免 override finalize():

finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是: 虚拟机调用GC的时间不确定以及Finalize daemon线程被调度到的时间不确定。

finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。

含有Finalize方法的object需要至少经过两轮GC才有可能被释放。

其他

内存泄漏检测工具强烈推荐 squareup 的 LeakCannary,但需要注意Android版本是4.4+的,否则会Crash。

Android程序内存测试框架leakcanary的使用:http://blog.csdn.net/wenzhi20102321/article/details/72943408

Java堆和栈的区别/联系详解http://blog.csdn.net/wenzhi20102321/article/details/78832250

共勉:只要路是对的,再难也要坚持。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: