在android开发中,总会不经意留下很多的内存泄露问题。下面就来谈谈这些问题的源头与解决方法。
Java相比于c/c++等其他语言的明显优点就是解决内存泄露的问题,所以内存优化就成为了java的重点解决内容。
android基于java开发,Java内存分配和回收都是由java 运行环境(JRE)在后台回收那些不在使用的内存,即GC垃圾回收机制。尽管Android系统的虚拟机拥有自动回收垃圾的机制,但这并不代表我们就可以忽视应该在什么时候分配和释放内存。
我们要做到的是,当对象不在需要使用的时候,使对象的引用不再被其它对象所持有,让系统自动这个对象所分配的内存进行回收。
内存泄露:
无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。
Java内存泄露根本原因:
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。(通常都是由于全局成员变量持有对象引用所导致的)
这里来模拟一下非静态类内存泄露
public class MainActivity extends AppCompatActivity {
List<String> strings = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakClass leakClass = new LeakClass();
leakClass.start();
}
class LeakClass extends Thread {
@Override
public void run() {
while (true) {//这个线程会一直存在,生命周期长
try {
//为了看得到明显的内存分配,这里new 出比较多的对象
for (int i = 0; i < 100000;i++)
strings.add(new String(i+""));//非静态类会持有外部类的引用,可以直接访问外部类属性成员,所以这里的strings就是外部类的strings。
Thread.sleep(60 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
我们频繁的back键退出应用,再打开应用(或者频繁切换横屏竖屏),多操作几十次,就应用奔溃了。
我们可以发现,曲线一直上升,并没有下降,意思就是内存分配一直在增加,而没有得到回收,也就是内存泄露了。
我们也可以观察gc的输出日志
我们只看free 42521K/49512K 这一部分,左边代表当前已经分配的内存,我们可以发现,一直都在增长,没有下降,内存泄露了。
下面采用静态内部类+弱应用
public class MainActivity extends AppCompatActivity {
List<String> strings = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakClass leakClass = new LeakClass(strings);
leakClass.start();
}
//这里static 类
static final class LeakClass extends Thread {
WeakReference<List<String>> weakReference;//弱引用
public LeakClass(List<String> weakReference) {
this.weakReference = new WeakReference<List<String>>(weakReference);
}
@Override
public void run() {
while (true) {//长生命周期
try {
for (int i = 0; i < 100000; i++)
weakReference.get().add(new String(i + ""));
Thread.sleep(60 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
做同样的操作
曲线一直呈波浪形,每一次都有回收资源。内存分配不会一直增长。
从11007K/24024K到 17100K/24024K,然后又回到11007K/24024K。
这就是系统对资源的回收,所以怎么也升不上去。
为什么使用弱引用和静态内部类就可以解决这个内存泄露,看完后面的知识,估计你就会明白了。
下面列出一些常见的内存泄露场景:
1.静态集合类引起内存泄露:像HashMap、Vector等的使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致的,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null; //只是o变量没引用对象,v对象还引用这对象。
}
- 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("xx",25);
Person p2 = new Person("yy",26);
Person p3 = new Person("zz",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(set.size()); //结果:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println(set.size()); //结果:4 个元素!
3.监听器
在java 编程中,我们会调用一个控件的addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
4.各种连接
比如数据库连接connection,网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
5.内部类和外部模块等的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
public void registerMsg(Object b);
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
6.单例模式
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。
7.非静态内部类内存泄露(Handler 内存泄露)
如果我们在一个类中定义了一个非静态的内部类(匿名类),那么内部类和外部类之间是相互持有引用的。因此在匿名内部类或者非静态内部类可以直接访问外部类的方法和私有变量。
当一个Android应用程序第一次启动时,Android框架为应用程序的主线程创建一个Looper对象。一个Looper实现了一个简单的消息队列,在一个循环中处理Message对象。所有主要的应用程序框架事件(如Activity生命周期方法的调用,单击按钮,等等)都包含在Message对象中,它被添加到Looper的消息队列然后一个个被处理。主线程的Looper在应用程序的整个生命周期中都存在。
当一个Handler在主线程中被实例化,它就被关联到Looper的消息队列。每个被发送到消息队列的消息会持有一个Handler的引用,以便Android框架可以在Looper最终处理这个消息的时候,调用这个Message对应的Handler的handleMessage(Message)。
所以,一个handler发出了message信息,message自身携带该handler的引用,而Looper消息队列引用这这个message会一直存在,引用的handler也会一直存在。而非静态handler会持有外部类的引用,通常是activity。这时候,如果message是延时处理,那么就会导致activity得不到及时的释放直到message被处理掉,而导致内存泄露。
这里模拟一下handler的内存泄露:
public class MainActivity extends AppCompatActivity {
List<String> strings = new ArrayList<>();
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//MainActivity.this.finish();
this.removeMessages(0);
}
};
private static Haha haha;//只要应用进程还在,这个静态变量(不为空)就不会释放。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
haha = new Haha();
for (int i = 0; i < 200000; i++)//为了看到明显的内存分配,创建比较多的对象.
strings.add(new String(i + ""));
mHandler.sendEmptyMessageDelayed(0, 100000);
}
class Haha {//非静态内部类,会对外部对象有一个引用
}
}
频繁打开关闭应用程序之后,我们看看结果:
内存分配呈上升趋势,内存泄露严重,因为handler延迟发送。
下面是正确的使用模板:
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);//handler 会把runnable包装成message对象发送出去。
// 返回前一个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
1.static块可以出现类中的任何地方(只要不是方法内部,记住,任何方法内部都不行),并且执行是按照static块的顺序执行的。
2.即使没有显示地声明为static,类的构造器实际上也是静态方法。
类加载执行顺序:
1.从父类到子类,把static的都先加载(static按代码顺序执行)。
2.从父类到子类,把构造函数加载(有初始化的非静态成员也属于构造函数内容,并且排在造函数里面的代码前面)。
一、静态变量在类被加载的时候分配内存。
当我们启动一个app的时候,系统会创建一个进程,此进程会加载一个Dalvik VM的实例,然后代码就运行在DVM之上,类的加载和卸载,垃圾回收等事情都由DVM负责。也就是说在进程启动的时候,类被加载,静态变量被分配内存。
二、静态变量在类被卸载的时候销毁。
类在进程结束的时候被卸载。
说明:一般情况下,所有的类都是默认的ClassLoader加载的,只要ClassLoader存在,类就不会被卸载,而默认的ClassLoader生命周期是与进程一致的(一般情况下)。
三、Android中的进程什么时候结束。
这个是Android对进程和内存管理不同于PC的核心——如果资源足够,Android不会杀掉任何进程,另一个意思就是进程随时可能会被杀掉。而Android会在资源够的时候,重启被杀掉的进程。也就是说静态变量的值,如果不做处理,是不可靠的,可以说内存中的一切都不可靠。如果要可靠,还是得保存到Nand或SD卡中去,在重启的时候恢复回来。
另一种情况就是不能把退出所有Activity等同于进程的退出,所以在用户点击图标启动应用的时候,以前存放于静态变量中的值,有可能还存在,因此要视具体情况给予清空操作。
四、Application也是一样不可靠。
Application其实是一个单例对象,也是放在内存中的,当进程被杀掉,就全清空了,只不过Android系统会帮重建Application,而我们存放在Application的数据自然就没有了,还是得自己处理。
五、静态引用的对象不会被垃圾回收。
只要静态变量没有被销毁也没有置null,其对象一直被保持引用,也即引用计数不可能是0,因此不会被垃圾回收。因此,单例对象在运行时不会被回收。
在dalvik虚拟机中,static变量所指向的内存引用,如果不把它设置为null,GC是永远不会回收这个对象的。
在Andorid 程序中:
1.默认每一个应用程序都运行于自己的Linux进程。
程序的任意代码被执行,就会启动一个进程。
当不需要此进程或者其他程序需要系统资源,则关闭此进程。
2.每个进程运行于自己的java虚拟机。(也就是说,应用程序代码和其他程序代码是隔绝的)
3.默认一个程序被赋予唯一Linux用户ID,加以权限设置,似的程序的文件仅仅对该用户,该程序可见。(其他方法也可以使得这些文件能给塔器程序访问)
总结安卓开发中需要注意的事项:
1.不在一个Activity中使用非静态内部类, 以防它的生命周期比Activity长。并且,尽量使用持有Activity弱引用的静态内部类。
2.根据目标的生命周期长短,传applicationContext,或传context。
3.当界面不可见时,对资源进行释放回收。(置为null或者close资源)
4.不到必要的时候,不使用service,或者采用IntentService代替service(可以控制Service的生命周期)。
5.避免在Bitmap上浪费内存。
6.使用优化过的数据集合。
7.应当尽可能地不使用枚举。
参考
Android最佳性能实践(一)——合理管理内存
http://blog.csdn.net/guolin_blog/article/details/42238627
Handler导致内存泄露
http://www.cnblogs.com/kissazi2/p/4121852.html
Static 详解以及面试题
http://www.cnblogs.com/dolphin0520/p/3799052.html