浅谈并小结java内存泄漏

一.定义

首先什么是内存泄漏,简单点说就是用完了忘了回收,而其他对象等资源想用却没法用的一种“站着茅坑不拉屎”的浪费资源的情况。在C/C++中,多数泄漏的场景就是程序离开某一运行域时,如在某个方法体中new出的对象或者malloc出的结构体等,并且只有该方法体中的局部变量指向这块内存区域时,在该方法返回时,存在栈中的局部变量指针随着栈帧被一起销毁,那么就没有任何指针可以指向该内存区域了,那么这块内存区域便是泄漏了。

而java的内存泄漏呢?众所周知,java的内存回收是由gc管理的。gc运行的是可达性算法,jvm中的gc线程将java对象从一个特定的对象gcroot开始,看成一个树,遍历树以判断对象的可达性,和C++那种静态的指针计数拥有本质区别,不存在无用对象间相互引用造成的困扰。也就是说当java对象真正确实没有任何一个来自存活对象的(强)引用(弱引用等除外)的时候(如果如果引用来自不可达对象,那么该对象仍然是不可达的),java对象才可能会被回收。也就是说,这里的java内存泄漏和传统的C/C++是不尽相同的

下图是java的可达性算法:

二.什么是java的内存泄漏

个人理解的java内存泄漏,其实可以描述为:对象遗忘或者说是找不到引用。因为在java中如果对象还在heap中存活的话(死亡对象gc回收之前除外),那么必然在整个环境中有来自存活对象的引用指向这个对象,简而言之,就是“此人还活着必有其存在的意义”。
那么问题来了,那个引用在哪呢?我们开发者根本在代码里看不见啊?别急,这个就是java内存泄漏的关键所在。很多时候这些看不见的引用藏在了某框架中,某静态变量中,或者某还在运行的子线程中。。。。
下面举几个例子说不定你就明白了。
三.常见场景
1.HashMap,有时候,我们一不小心把可变hashcode的对象作为HashMap的key,当你一不小心改变了对象的hashcode的时候,key就无法取出对应的value,此value对于你便不可达了。但是value的引用还是存在与HashMap框架内部的数组和链表结构中。而你又无可奈何,内存便”泄漏“了。
2.Android中常见的Context泄漏(Activity代表)。在Android中,给Context对象引用要小心处理,究其根源是因为Android应用层框架帮我们管理了Activity(Context)对象的生命周期,这类差不多可以代表我们现在常用的IOC框架,框架管理对象的生灭,开发者只有应用权。如果你不小心将Context对象塞进了哪个静态变量中,或者引用到了哪个长周期的子线程中等,在Activity(Context)结束时,框架层跑完所有销毁程序,移除了它对Context的引用,这时框架层就认为这个Context已经死亡了。而框架却没有想到在他外部还有其他引用没有释放。这样就造成了泄漏。
3.静态变量,这个不用过多阐述,很好理解。
4.资源对象用完没有释放,如数据库,流这些资源类,如Cusor,File.Socket等。原因是因为这些与本地平台交互的接口涉及JNI,可能有native层的指针引用了java层的对象,没有释放的话,引用是一直存在的。
5 .非静态的内部类,尤见于Activity内部的Handler和Runnable等,和上一个阐述的关联起来。非静态内部类包含一个对外部类的强引用,而且这个引用是编译器自己加的,开发者在源码中看不见,故比较隐蔽。
如果你反编译一个在Activity中的内部类你会发现:
# instance fields
.field final synthetic this$0:Lcom/gy/just/VoltageMonitor/View/Activity/YunWeiActivity;

.field final synthetic val$sec:I


# direct methods
.method constructor <init>(Lcom/gy/just/VoltageMonitor/View/Activity/YunWeiActivity;I)V
    .locals 0
    .param p1, "this$0"    # Lcom/gy/just/VoltageMonitor/View/Activity/YunWeiActivity;
编译器自动给内部类加了一个叫“this$0”的外部类变量,并且在构造函数中赋值。那么内部类中就持有有了一个不可见的外部类强引用。像Runnable,Handler这类的内部类如果子线程没有结束的话,那么外部类对象的泄漏就容易发生了。
那么,我们来做一个有趣的小实验,我们如果把内部类中外部类的引用置空的话,会发生什么有趣的现象?
public class B extends A{
	
	public void test(){
		new Thread(new Demo()).start();
	}
	
	@Override
	public void dosth() {
		// TODO Auto-generated method stub
		System.out.println("doB");
	}
	
class Demo implements Runnable{
	
		
		public Demo(){
			try {
				Field field = getClass().getDeclaredField("this$0");
				field.setAccessible(true);
				field.set(this, null);
			} catch (NoSuchFieldException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		

		@Override
		public void run() {
			// TODO Auto-generated method stub
			
			
			dosth();
			
		}
	}
}
运行异常如下
public abstract interface java.util.List<E>
Exception in thread "Thread-0" java.lang.NullPointerException
	at test.B$Demo.run(B.java:46)
	at java.lang.Thread.run(Thread.java:745)

怎么样?内部类Demo已经无法调用外部类的dosth方法了,抛出空指针异常。

四.总结,严格意义上说java是没有真正的内存泄漏的,这些泄漏和我们看不见的框架内部息息相关,所以养成良好的编程习惯,理解理解常用框架的结构原理,就能避免这些错误。最后,如果大家觉得小弟理解的还不错的,不妨给个赞,道友们觉得理解上有错误的也欢迎指正,毕竟这是我的个人看法。




  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值