内存泄露
质有高低,境界不同------------ 2018.03.16
目录
- 1.内存泄露定义
- 2.内存泄露和内存溢出的对比
- 3.内存泄露产生的几种场景和如何检测内存泄露
- 4.避免或解决内存泄露的几种方法
1.内存泄露的定义
- Java的内存管理就是内存对象的分配和释放的问题。与C语言不同,Java通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存。在Java中当一个对象在不需要使用的时候,GC会在不定时的时间进行内存的回收,是不是就不存在内存管理的问题呢?答案是否定的,现在Andriod手机的运行内存尽管越来越大,当时Google在架够中分配给一个应用程序的内存是固定的,如果相关的内存回收不及时,就会导致程序崩溃的问题。另外,了解GC的相关回收机制的战友们都了解,对四种不同的引用GC的回收方式是不一样的,对于这个问题就不深入说明。
- 所谓内存泄露的定义就是,当一个对象已经不必要使用的时候依旧持有该对象的引用,而导致GC不能及时回收内存。
2. 内存泄露和内存溢出的对比
-
在面试中时常问到,内存溢出和内存泄露的相关问题。
内存泄露的问题相比于内存溢出的危害程度小一些,内存溢出大多是涉及到大图加载和处理方面,主流的框架这一部分的处理已经非常不错了,所以在实际问题中,内存泄露的问题出现的概率会高一些,比较会不引起注意。在处理高性能App中,如何避免内存泄露也是性能规范中的一个重要的研究课题。
3. 内存泄露出现的场景和如何检测内存泄露
-
3.1 出现内存泄露的场景
-
3.1.1 静态变量造成的内存泄漏
public class DBManager { private static DBManager mDBManager; private Context mContext; private DBManager(Context context){ mContext = context; } public static DBManager getInstance(Context context){ if(application==null){ mDBManager = new DBManager(context); } return mDBManager; } }单例模式是最常见的设计模式了,当Context的对象是一个Activity时,当Activity退出的时候,由于静态变量mDBManager的生命周期是随着类的加载而加载,直至到源程序的结束,mDBManager依旧持有Context的引用,导致这个Activity在堆中分配的内存不能被释放而导致内存泄露。
解决方案:使用Application的Context引用
-
3.1.2 .非静态内部类的静态实例
在Oracle的描述中, 补救一下静态内部类和非静态内部类的概念
Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes. 内部类分为两种,一种是静态的,一种是非静态的。使用static修饰的类叫静态内部类,另一种叫内部类 A nested class is a member of its enclosing class. Non-static nested classes (inner classes) have access to other members of the enclosing class, even if they are declared private. Static nested classes do not have access to other members of the enclosing class. As a member of the OuterClass, a nested class can be declared private, public, protected, or package private. (Recall that outer classes can only be declared public or package private.) 非静态内部类默认可以访问外部类的成员变量,即使是用private修饰的,作为外部类的成员,内部类可以声明为 private , public ,protected ,或者是 default.(外部类只能声明public 或者default ) Why Use Nested Class? Compelling reasons for using nested classes include the following: - It is a way of logically grouping classes that are only used in one place: If a class is useful to only one other class, then it is logical to embed it in that class and keep the two together. Nesting such "helper classes" makes their package more streamlined. 如果一个类是非常有用的对于另外一个类来说,使用内部类的方式是非常合理的,内嵌的方式能够使包更加合理化 - It increases encapsulation: Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A's members can be declared private and B can access them. In addition, B itself can be hidden from the outside world. 封装的良好体现.考虑两个顶层的类,A和B,当B需要访问A中包括private修饰的所有成员,通过将B隐藏在A中,A中的成员即使声明是private,B也是可以访问的,另外B自己也被隐藏在外部类A中。 - It can lead to more readable and maintainable code: Nesting small classes within top-level classes places the code closer to where it is used. 内嵌的方式可以增加代码的可读性和可维护性,将小的类可以让类之间更加的紧凑,使用时更加方便。 Static Nested Classes(静态内部类) As with class methods and variables, a static nested class is associated with its outer class. And like static class methods, a static nested class cannot refer directly to instance variables or methods defined in its enclosing class: it can use them only through an object reference.(一个静态内部类不能直接访问外部类的局部变量和方法 ,只能通过引用的方式) Note: A static nested class interacts with the instance members of its outer class (and other classes) just like any other top-level class. In effect, a static nested class is behaviorally a top-level class that has been nested in another top-level class for packaging convenience. (一个静态内部类和外部类的访问方式,和其他的类的是一样的,事实上,静态内部类更多意义体现在打包更加方便) Inner Class(内部类) As with instance methods and variables, an inner class is associated with an instance of its enclosing class and has direct access to that object's methods and fields. Also, because an inner class is associated with an instance, it cannot define any static members itself. (对于局部变量和方法, 一个内部类是外部类的一个实例并且可以直接访问对象的成员变量和方法。也因为这样,内部类不能声明任何的静态成员变量和方法)在梳理完这些概念之后,相信你对非静态内部类和静态内部类都有了一定的认识。下面看一段会引起内存泄露的代码:
public class WhiskyActivity extends Activity{ private static Test mTest; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(mTest==null){ mTest = new Test(); } } class Test{ } }从上面的例子中Test是一个内部类,new 出来的Test类默认持有WhiskyActivity的实例,然而mTest是静态的成员变量,生命周期是和整个源程序一样的,当Activity销毁了,会因为mTest依旧持有WhiskyActivity的实例不能释放,而导致内存泄露。
解决方案:使用静态内部类
-
3.1.3 不正确使用线程,造成内存泄漏
在使用AsyncTask,Timer,ThreadPool,IntentService,开启线程一般需要执行耗时操作,当Activity销毁之后,线程依然在执行,持有Activity的引用
public class TimerActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_2); findViewById( R.id.finish2).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); //开始定时任务 timer(); } void timer(){ new Timer().schedule(new TimerTask() { @Override public void run() { while(true); } },1000 ); // 1秒后启动一个任务 } }上述例子中Timer中的TimerTask还在执行是Activity销毁,Timer和TimerTask依旧持有TimerActivity 的引用
解决方案:在适当时候cancel任务,TimerTask使用静态类
-
3.1.4 Handler引起的内存泄露
Handler是Android常用的消息机制,使用的时候不注意常常会引起内存泄露,在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。以下是会内存泄露的一个实例:
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); // Post a message and delay its execution for 10 minutes. mLeakyHandler.postDelayed(new Runnable() { @Override public void run() { /* ... */ } }, 5000); // Go back to the previous Activity. finish(); } }解决方案: 使用软引用和及时释放资源,如下:
public class MainActivity extends AppCompatActivity { private MyHandler mHandler = new MyHandler(this); private TextView mTextView ; private static class MyHandler extends Handler { //软引用 private WeakReference<Context> reference; public MyHandler(Context context) { reference = new WeakReference<>(context); } @Override public void handleMessage(Message msg) { MainActivity activity = (MainActivity) reference.get(); if(activity != null){ //记得判空 activity.mTextView.setText(""); } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView)findViewById(R.id.textview); loadData(); } private void loadData() { //...request Message message = Message.obtain(); mHandler.sendMessage(message); } //注意释放 @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); } } -
3.1.5 .系统资源或外部资源的没有及时回收
- Bitmap资源未及时回收
- IO流未关闭
- Cursor使用完后未释放
- EventBus ,广播未反注销
- 各种连接(网络,数据库,socket等)
解决方案:及时关闭资源,bitmap调用recycler方法
-
3.1.6 .监听器注册造成的内存泄漏
观察者模式中有一个统一的观察者collector集合,事件在回调监听。如果一个类注册了监听器,但当该类不再被使用后没有注销监听器,可能会发生内存泄漏。例如,系统的传感器sensor监听器, 窗口改变监听WindowFocusChangeListener等等
SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL); //监听 sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);解决方案:不使用时及时移除监听
protected void onPause() { super.onPause(); mSensorManager.unregisterListener(this); } -
3.1.7 WebView引起的内存泄露
在使用Hybird开发中,我们会使用Webviewq去加载Html网页,如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。及时Activity关闭时在onDestroy中调用如下代码也是没有任何作用。
解决方案:在Activity销毁的时候及时调用如下代码释放资源
private void destroyWebView() { if (mWebView != null) { mLinearLayout.removeView(mWebView); mWebView.pauseTimers(); mWebView.removeAllViews(); mWebView.destroy(); mWebView = null; } } -
-
3.2 如何检测内存泄露
-
LeakCanary是现在检测内存泄露最方便的工具 LeakCanary git地址
-
使用leakCanary检测项目中内存泄露这需要以下几步
- 第一,在项目的build.gradle文件中添加一下依赖:
dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' }- 第二步,在你的Application中添加如下代码:
public class ExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this); // Normal app init code... } }- 第三步,运行程序,在你应用的边上就会出现如下标识
在出现内存泄露的时候,会有如下界面:
-
4. 总结避免内存泄露的几种方法
-
4.1 static对象的生命周期过长,应该谨慎使用,使用时注意生命周期的影响,对于Context的引用,除Dialog的情况必须传入Activity的Context,考虑生命周期的影响,可以传入Application的Context
-
4.2 系统资源在使用完毕之后要记得及时关闭
-
4.3 .内部类在使用的时候,尽量考虑使用静态内部类的形式
-
4.4 . 对于Bitmap和Activity比较常出现的内存泄露的大户,可以考虑使用软引用(使用时候注意判空),Bitmap可以调用,在不使用时手动回收资源
-
4.5 多线程引起的内存泄露,注意线程任务的关闭
-
4.6 .及时注销监听
-
4.7 . 使用轻量的数据结构
-
使用ArrayMap/SparseArray来代替HashMap,ArrayMap/SparseArray是专门为移动设备设计的高效的数据结构。
-
HashMap内部使用一个默认容量为16的数组来存储数据,采用拉链法解决hash冲突(数组+链表),如下图:
-
SparseArray
- 支持int类型,避免自动装箱,但是也只支持int类型的key
- 内部通过两个数组来进行数据存储的,一个存储key,另外一个存储value
- 因为key是int,在查找时,采用二分查找,效率高,SparseArray存储的元素都是按元素的key值从小到大排列好的。 (Hashmap通过遍历Entry数组来获取对象)
- 默认初始size为0,每次增加元素,size++
-
ArrayMap
- 跟SparseArray一样,内部两个数组,但是第一个存key的hash值,一个存value,对象按照key的hash值排序,二分查找也是按照hash
- 查找index时,传入key,计算出hash,通过二分查找hash数组,确定index
-
-
4.8 不要使用String进行字符串拼接
- 严格的讲,String拼接只能归结到内存抖动中,因为产生的String副本能够被GC,不会造成内存泄露。
- 频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。