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

Android 产生内存泄露的原因以及解决办法

2017-08-30 11:17 316 查看
内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收。最近自己阅读了大量相关的文档资料,打算做个 总结 沉淀下来跟大家一起分享和学习,也给自己一个警示,以后 coding 时怎么避免这些情况,提高应用的体验和质量。 

我会从 Java 内存泄漏的基础知识开始,并通过具体例子来说明 Android 引起内存泄漏的各种原因,以及如何利用工具来分析应用内存泄漏,最后再做总结。 

篇幅有些长,大家可以分几节来看!


Java 内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。 
静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。 
栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 
堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。 
栈与堆的区别: 

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。 

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

public class Simple {
int s1 =0;
Simple  simple = new Simple();
public void method(){
int s2 = 1;
Simple mSimple = new Simple();
}

}
Simple simple3 = new Simple();


Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。 

mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。 
结论: 

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。 

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。 

了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。


Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC
都需要进行监控。 

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。 

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。 

以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。 



ava使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。


什么是Java中的内存泄露

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

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。 

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。 



因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。 
对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot
JVM就支持这一特性。 
同样给出一个 Java 内存泄漏的典型例子, 

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


在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。


Android中常见的内存泄漏汇总

一、集合类泄露:

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际上我们在项目中肯定不会写这么烂的代码,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。

这个比较好理解,如果一个对象放入到
ArrayList
HashMap
等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合
remove
,或者
clear
集合,以避免内存泄漏。

二、单例造成的内存泄漏

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

public class AppSettings {

private static AppSettings sInstance;
private Context mContext;

private AppSettings(Context context) {
this.mContext = context;
}

public static AppSettings getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppSettings(context);
}
return sInstance;
}


像上面代码中这样的单例,如果我们在调用
getInstance(Context context)
方法的时候传入的
context
参数是
Activity
Service
等上下文,就会导致内存泄露。
Activity
为例,当我们启动一个
Activity
,并调用
getInstance(Context context)
方法去获取
AppSettings
的单例,传入
Activity.this
作为
context
,这样
AppSettings
类的单例
sInstance
就持有了
Activity
的引用,当我们退出
Activity
时,该
Activity
就没有用了,但是因为
sIntance
作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个
Activity
的引用,导致这个
Activity
对象无法被回收释放,这就造成了内存泄露。
为了避免这样单例导致内存泄露,我们可以将
context
参数改为全局的上下文:
private AppSettings(Context context) {
this.mContext = context.getApplicationContext();
}

全局的上下文
Application Context
就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。
单例模式对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用
Activity
的上下文,而是使用
Application
的上下文。
三、静态变量导致内存泄露
静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。
比如下面这样的情况,在
Activity
中为了避免重复的创建
info
,将
sInfo
作为静态变量:
public class MainActivity extends AppCompatActivity {

private static Info sInfo;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sInfo != null) {
sInfo = new Info(this);
}
}
}

class Info {
public Info(Activity activity) {
}
}

Info
作为
Activity
的静态成员,并且持有
Activity
的引用,但是
sInfo
作为静态变量,生命周期肯定比
Activity
长。所以当
Activity
退出后,
sInfo
仍然引用了
Activity
Activity
不能被回收,这就导致了内存泄露。
在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。
四、非静态内部类导致内存泄露
非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用
Handler
,很多开发者在使用
Handler
是这样写的:
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start();
}

private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}

private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// 做相应逻辑
}
}
};
}

也许有人会说,
mHandler
并未作为静态变量持有
Activity
引用,生命周期可能不会比
Activity
长,应该不一定会导致内存泄露呢,显然不是这样的!
熟悉
Handler
消息机制的都知道,
mHandler
会作为成员变量保存在发送的消息
msg
中,即
msg
持有
mHandler
的引用,而
mHandler
Activity
的非静态内部类实例,即
mHandler
持有
Activity
的引用,那么我们就可以理解为
msg
间接持有
Activity
的引用。
msg
被发送后先放到消息队列
MessageQueue
中,然后等待
Looper
的轮询处理(
MessageQueue
Looper
都是与线程相关联的,
MessageQueue
Looper
引用的成员变量,而
Looper
是保存在
ThreadLocal
中的)。那么当
Activity
退出后,
msg
可能仍然存在于消息对列
MessageQueue
中未处理或者正在处理,那么这样就会导致
Activity
无法被回收,以致发生
Activity
的内存泄露。
通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。
public class MainActivity extends AppCompatActivity {

private Handler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
start();
}

private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}

private static class MyHandler extends Handler {

private WeakReference<MainActivity> activityWeakReference;

public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
if (msg.what == 1) {
// 做相应逻辑
}
}
}
}
}

mHandler
通过弱引用的方式持有
Activity
,当GC执行垃圾回收时,遇到
Activity
就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。
上面的做法确实避免了
Activity
导致的内存泄露,发送的
msg
不再已经没有持有
Activity
的引用了,但是
msg
还是有可能存在消息队列
MessageQueue
中,所以更好的是在
Activity
销毁时就将
mHandler
的回调和发送的消息给移除掉。
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}

非静态内部类造成内存泄露还有一种情况就是使用
Thread
或者
AsyncTask

比如在Activity中直接
new
一个子线程
Thread

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
// 模拟相应耗时逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

或者直接新建
AsyncTask
异步任务:
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// 模拟相应耗时逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
}

很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程
Thread
AsyncTask
都是匿名内部类对象,默认就隐式的持有外部
Activity
的引用,导致
Activity
内存泄露。要避免内存泄露的话还是需要像上面
Handler
一样使用静态内部类+弱应用的方式(代码就不列了,参考上面
Hanlder
的正确写法)。
再看如下例子

public class MainActivity extends Activity{

private static TestResource msource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (msource =null){
msource = new TestResource();
}
}
class TestResource{
//......
}
}
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下: 
这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为: 
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下: 



五、未取消注册或回调导致内存泄露
比如我们在
Activity
中注册广播,如果在
Activity
销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有
Activity
引用,导致内存泄露。因此注册广播后在
Activity
销毁后一定要取消注册。
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.registerReceiver(mReceiver, new IntentFilter());
}

private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 接收到广播需要做的逻辑
}
};

@Override
protected void onDestroy() {
super.onDestroy();
this.unregisterReceiver(mReceiver);
}
}

在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用
Retrofit+RxJava
注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。
六、Timer和TimerTask导致内存泄露
Timer
TimerTask
在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的
ViewPager

public class MainActivity extends AppCompatActivity {

private ViewPager mViewPager;
private PagerAdapter mAdapter;
private Timer mTimer;
private TimerTask mTimerTask;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 3000, 3000);
}

private void init() {
mViewPager = (ViewPager) findViewById(R.id.view_pager);
mAdapter = new ViewPagerAdapter();
mViewPager.setAdapter(mAdapter);

mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
loopViewpager();
}
});
}
};
}

private void loopViewpager() {
if (mAdapter.getCount() > 0) {
int curPos = mViewPager.getCurrentItem();
curPos = (++curPos) % mAdapter.getCount();
mViewPager.setCurrentItem(curPos);
}
}

private void stopLoopViewPager() {
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
stopLoopViewPager();
}
}

当我们
Activity
销毁的时,有可能
Timer
还在继续等待执行
TimerTask
,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即
cancel
Timer
TimerTask
,以避免发生内存泄漏。
七、资源未关闭或释放导致内存泄露
在使用
IO
File
流或者
Sqlite
Cursor
等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。
八、属性动画造成内存泄露
动画同样是一个耗时任务,比如在
Activity
中启动了属性动画(
ObjectAnimator
),但是在销毁的时候,没有调用
cancle
方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用
Activity
,这就造成
Activity
无法正常释放。因此同样要在
Activity
销毁的时候
cancel
掉属性动画,避免发生内存泄漏。
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}

九、WebView造成内存泄露
关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的
destory()
方法来销毁它以释放内存。
另外在查阅
WebView
内存泄露相关资料时看到这种情况:

Webview
下面的
Callback
持有
Activity
引用,造成
Webview
内存无法释放,即使是调用了
Webview.destory()
等方法都无法解决问题(Android5.1之后)。

最终的解决方案是:在销毁
WebView
之前需要先将
WebView从
父容器中移除,然后在销毁
WebView
。详细分析过程请参考这篇文章:WebView内存泄漏解决方法
@Override
protected void onDestroy() {
super.onDestroy();
// 先从父控件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}

总结
内存泄露在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避免大多数情况的内存泄漏:
构造单例的时候尽量别用
Activity
的引用;

静态引用时注意应用对象的置空或者少用静态引用;

使用静态内部类+软引用代替非静态内部类;

及时取消广播或者观察者注册;

耗时任务、属性动画在
Activity
销毁时记得
cancel


文件流、
Cursor
等资源及时关闭;
Activity
销毁时
WebView
的移除和销毁。

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