1.Java内存泄漏基础知识
1.何为内存泄漏?
简单的说:就是该释放的对象没有释放,一直被某个或某些实例所持有却不在被使用导致GC不能回收。
2.何为内存溢出(OutOfMemory)?
当系统中的内存使用达到上限,进行gc的时候,有大量的内存溢出的情况出现。导致系统不能正常回收内存。从而出现内存溢出的异常。
3.java的内存分配策略:
java程序运行的内存分配策略有3种,分别是静态分配,栈式分配,和堆式分配。对应着3种存储策略使用的存储空间主要分别是静态存储区(也称方法区),栈区和堆区。
- 静态存储区(也称方法区): 主要存放静态数据,全局static数据和常量。这块内存在程序编译时已经分配好,并且在程序整个运行期间都存在。
- 栈区: 当方法被执行时,方法体内的局部变量都在栈中创建。并且方法执行结束时这些变量所持有的内存将会自动被释放。 因为栈内存分配的运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
- 堆区: 又称为动态内存分配,通常是指在程序运行时直接new出来的内存。这部分内存不使用时将会有java垃圾回收器来负责回收
栈区和堆区的区别:
在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当一段方法块中的定义一个变量时,java就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量就无效了。分配给他的内存空间也将被释放掉,该内存空间可以被重新使用。
堆内存用于存放所有由new创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由java垃圾回收器来自动管理。在堆中产生的一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。
例:
public class Sample() {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Simple类中的局部变量s2和引用变量mSample2都存在于栈中,而mSample2指的实体存在于堆中,包括这个对象中所有的成员变量s1和mSimple1。
结论:
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存在于堆中。--因为它们属于方法的变量,生命周期随着方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)。--因为它们属于类,类对象终究是要被new出来使用的。
**java的内存泄漏只能是堆内存泄漏**
4.java的对象的4种引用类型
Java对引用的分类有 Strong reference, SoftReference, WeakReference, PhatomReference 四种。
在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软/弱引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软/弱引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,可以根据引用队列判断软/弱引用所引用的对象所引用的对象是否被回收。
1.软引用实例
handler的内存泄漏有
2.弱引用的实例
假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生:
首先定义一个HashMap,保存软引用对象。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再来定义一个方法,保存Bitmap的软引用到HashMap。
软/弱引用的总结:
使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。
如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
2.android常见的内存问题
1. 集合类泄漏
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。
2. 单例造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。
正确的方式应该改为下面这种方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
3. 匿名内部类/非静态内部类默认持有外部类的对象,不小心使用很有可能导致内存溢出
例:
- 非静态内部类创建静态实例造成的内存泄漏
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mResource == null){
mResource = new TestResource();
}
//...
}
class TestResource {
//...
}
}
这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例。 静态内部类不持有外部类的实例,单例类和activity本身没有关系。
- 匿名内部类被异步线程持有导致的内存泄漏
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:
但ref2这个匿名类的实现对象里面多了一个引用:
this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。
4. handler造成的内存泄漏
handler造成的内存泄漏的主要原因是Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。 如果handler对象持有activity的对象时就要造成内存泄漏。
Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有
由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。
举个例子:
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() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。
修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
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) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
5. 尽量避免使用static成员变量
如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。
这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。
这里修复的方法是:
不要在类初始时初始化静态成员。可以考虑lazy初始化。(编译时只是给静态资源分配了一部分内存,这个内存空间是可变的)。
架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。
6. 资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
7. 一些不良代码造成的内存泄漏
有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
比如:
Bitmap 没调用 recycle()方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。
构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。
3.内存泄漏检测工具
LeakCanary 中文使用说明
LeakCanary: 让内存泄露无所遁形
https://github.com/square/leakcanary
4.结合项目实例总结内存泄漏的问题
1.handler对象持有activity造成的内存泄漏
2.单例对象持有context造成的内存泄漏
3.互联课堂大量创建线程对象,并且不释放,造成的内存溢出。
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again