一.定义
首先什么是内存泄漏,简单点说就是用完了忘了回收,而其他对象等资源想用却没法用的一种“站着茅坑不拉屎”的浪费资源的情况。在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方法了,抛出空指针异常。