Java/Android内存泄漏(Memory Leak)(6种情况)

尊重劳动成果,转载请注明出处:https://blog.csdn.net/Emmanuel__/article/details/83792868

一、概述:Java内存泄露引起原因

    内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。 内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory


二、Java内存回收机制

    Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在 堆(Heap) 中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在Java语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值null,以下再没有调用过,另一个是给对象赋予了新值,这样重新分配了内存空间。
    java通过 可达性分析算法 判断对象是否存活。这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。


三、内存泄漏场景

    长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。

(1) 静态集合类引起内存泄露

    像HashMapVector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

Static Vector  v = new Vector(10); 
for (int i = 1; i<100; i++) { 
	Object o = new Object(); //注意这里o仅仅是对象的引用,引用,引用!!!
	v.add(o); 
	o = null; //所以这里仅释放引用本身并不能完成回收
}

   在这个例子中,循环申请Object对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null
解决方法: 在集合元素使用之后从集合中删除,等所有元素都使用完之后,将集合置空。

v.clear();
v = null;
(2) 当集合里面的对象属性被修改后,再调用remove()方法时不起作用
public static void main(String[] args) { 
	Set<Person> set = new HashSet<Person>(); 
	Person p1 = new Person("唐僧","pwd1",25); 
	Person p2 = new Person("孙悟空","pwd2",26); 
	Person p3 = new Person("猪八戒","pwd3",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 个元素! 
	for (Person person : set) 
		System.out.println(person); 
}

   直接修改对象的属性值,会导致该对象的hashcode值不同,从而导致set.remove(p3);失效。
    正确方法应该是先Person p3=set.remove(p3);修改完属性之后再set.add(p3)

(3) 监听器

   在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

Android:

手动注册广播时,退出时忘记 unregisterReceiver()
Service 执行完后忘记 stopSelf()
EventBus 等观察者模式的框架忘记手动解除注册
ListView 的 Item 泄露

(4) 各种连接

   比如 数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。 对于ResultsetStatement对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,ResultsetStatement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

(5) 内部类和外部模块等的引用

    内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。 此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
   public void registerMsg(Object b);
   这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就***需要注意模块B 是否提供相应的操作去除引用。***

(6) 单例模式

   不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,单例模式由于其静态特性,其生命周期 = 应用程序的生命周期, 考虑下面的例子:

class A{ 
	public A(){ 
		B.getInstance().setA(this); 
	} 
	.... 
} 
//B类采用单例模式 
class Singleton{ 
	private A a; 
	private static Singleton instance=new Singleton(); 
	private Singleton(){} 
	public static Singleton getInstance(){ 
		return instance; 
	} 
	public void setA(A a){ 
		this.a=a; 
	} 
	//getter... 
} 

    显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。 想象下如果A是个比较复杂的对象或者集合类型会发生什么情况?
引申小知识:
   通过反射破坏单例模式:

public class Test {
    public static void main(String[] args) throws Exception{
        Singleton s1 = B.getInstance();
 
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();
 
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
    }
}

   输出结果:

671631440
935563443

    如何避免:

public class Singleton {
    private static int count = 0;
 
    private static Singleton instance = null;
 
    private Singleton(){
        synchronized (Singleton.class) {
            if(count > 0){
                throw new RuntimeException("创建了两个实例");
            }
            count++;
        }
    }
 
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
 
    public static void main(String[] args) throws Exception {
 
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s1 = constructor.newInstance();
        Singleton s2 = constructor.newInstance();
    }
}

    输出结果:

Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at com.yzz.reflect.Singleton.main(Singleton.java:33)
Caused by: java.lang.RuntimeException: 创建了两个实例
    at com.yzz.reflect.Singleton.<init>(Singleton.java:14)
    ...

不是写Android客户端的java程序猿看到这里即可。。。


四、Android内存泄漏

1、单例模式
    很多时候我们在需要用到 Activity 或者 Context 的地方,会直接将 Activity 的实例作为参数传给对应的类,就像这样:

public class Singleton { 
	private static Singleton sInstance; 
	private Context mContext; 
	private SingletonTest(Context context){ 
		this.mContext = context;
	} 
	public static Singleton getInstance(Context context){ 
		if(sInstance == null){ 
			sInstance = new Singleton(context); 
		} 
		return sInstance; 
	} 
} 
// 外部调用 
Singleton sInstance = getInstance(MainActivity.this);

    外部传入一个 Context 来获取该类的实例,此时传入的 Context 是 Activity ,此时单例就有持有该 Activity 的强引用(直到整个应用生命周期结束)。这样的话,即使该 Activity 退出,该 Activity 的内存也不会被回收,这样就造成了内存泄露,特别是一些比较大的 Activity,甚至还会导致 OOM(Out Of Memory)。
   解决办法:单例模式引用的对象的生命周期 = 应用生命周期

public class Singleton { 
	private static Singleton sInstance; 
	private Context mContext; 
	private Singleton(Context context){ 
		this.mContext = context.getApplicationContext(); 
	} 
	public static Singleton newInstance(Context context){ 
		if(sInstance == null){ 
			sInstance = new Singleton(context); 
		} 
		return sInstance; 
	} 
}

   可以看到在 Singleton 的构造函数中,将 context.getApplicationContext() 赋值给 mContext,此时单例引用的对象是 Application,而 Application 的生命周期本来就跟应用程序是一样的,也就不存在内存泄露。
2、内部类
   先来看看非静态内部类(non static inner class)和 静态内部类(static inner class)之间的区别

class 对比static inner classnon static inner class
与外部 class 引用关系如果没有传入参数,就没有引用关系自动获得强引用
被调用时需要外部实例不需要需要
能否调用外部 class 中的变量和方法不能
生命周期自主的生命周期依赖于外部类,甚至比外部类更长

   非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这便埋下了内存泄露的隐患。 如果一个 Activity 的非静态内部类的生命周期比 Activity 更长,那么 Activity 的内存便无法被回收,也就是发生了内存泄露,而且还有可能发生难以预防的空指针问题。

public class MainActivity extends AppCompatActivity { 
	private Handler mHandler = new Handler(){ 
		@Override 
		public void handleMessage(Message msg) { 
			super.handleMessage(msg); 
		} 
	}; 
	@Override 
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState); 
		setContentView(R.layout.activity_main); 
		// ① 匿名线程持有 Activity 的引用,进行耗时操作 
		new Thread(new Runnable() { 
			@Override 
			public void run() { 
				try { 
					Thread.sleep(50000); 
				} 
				catch (InterruptedException e) { 
					e.printStackTrace(); 
				} 
			} 
		}).start(); 
	// ② 使用匿名 Handler 发送耗时消息 
	Message message = Message.obtain(); 
	mHandler.sendMessageDelayed(message, 60000); 
}

   1、new 出一个匿名的 Thread,进行耗时的操作,如果 MainActivity 被销毁而 Thread 中的耗时操作没有结束的话,便会产生内存泄露
   2、new 出一个匿名的Handler,这里我采用了 sendMessageDelayed() 方法来发送消息,这时如果 MainActivity 被销毁,而 Handler里面的消息还没发送完毕的话,Activity 的内存也不会被回收
解决方法:
   继承 Thread 实现静态内部类
   继承 Handler 实现静态内部类,以及在ActivityonDestroy()方法中,移除所有的消息 mHandler.removeCallbacksAndMessages(null);
   如果静态内部类确实需要持有外部类的引用,可使用弱引用:

public class Sample { 
	private WeakReference<Context> mWeakReference; 
	public Sample(Context context){ 
		this.mWeakReference = new WeakReference<>(context); 
	} 
	public Context getContext() { 
		if(mWeakReference.get() != null){ 
			return mWeakReference.get(); 
		} 
		return null; 
	} 
} 
// 外部调用 
Sample sample = new Sample(MainActivity.this);

   被弱引用关联的对象只能存活到下一次垃圾回收之前,也就是说即使 Sample 持有 Activity的引用,但由于 GC 会帮我们回收相关的引用,被销毁的Activity 也会被回收内存,这样我们就不用担心会发生内存泄露了。
3、关于static
   ***静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。***一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。
   在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。
4、其它
   监听器:Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。
   属性动画:动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle(),虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() {
	super.onDestroy();
	this.unregisterReceiver(mReceiver); //注销广播 
	mAnimator.cancel();//取消动画
	......
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值