闲来没事在日志中瞟见了个OutOfMemoryError错误,不由得想到前一段时间看到一篇问到Java中是否有内存泄露,这个很久以前是留意过的,大体记得内存溢出和内存泄露是不同的,至于各自都有哪些情况,那个...额....忘了...。好吧,记忆力一向不好,忘就忘了,那就再总结一遍吧。翻了下收藏的博客,回顾了下便是想起了了~.~。,但是其中的一个例子突然使我困惑了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
publicclassTestDemo {
staticTest[] tests = newTest[3];
publicstaticvoidmain(String[] args) {
Test t = newTest("test1");
tests[0] = t;
//将t置为null,看起来似乎我们已经释放创建的对象,当下次gc时其将被回收t = null;
//那么我们打印下test[0]看看System.out.println(tests[0]);
}
}
这是个示例内存泄露的例子,该例子十分典型,几乎所有内存泄露的示例都与此类似,作为javaer往往觉得理所应当。然而作为一个学习C++入行(学的很烂),并一直把引用当指针看的javaer不免觉得有些疑惑:t是对象的引用,这里可以看做指向对象的指针,那么test[0]=t,按理说应该是把t指针赋值给test[0],算是地址传递吧,那个t指向null之后,test[0]应该也指向null了啊。看起来似乎有点道理,然而当了解了java的引用之后,发现吧指针等同于引用是有一些问题的。
java中的引用到底是什么呢,简单点说,引用就是存在栈区的一种特定类型的数据,其存储着对象实例在堆区的地址,其特点如下:
·本身是一种数据类型,存储在栈区
·其值存储着实例对象在堆区的虚拟地址(注意,是虚拟地址,并不是实际内存地址,就如同图书馆里的索引号,不经过转换你并不知道书的实际位置)
·对象在创建未赋值时(无实例),引用会指向null
·java中参数传递只有值传递一种,所谓的引用传递(准确说是共享传递)传递的是引用中存储的值
从定义看起来似乎还是区分不出来引用到底和指针有什么区别,那么请注意上边红字,java为了屏蔽对内存直接操作,对对象的实际内存地址进行了包装,从而使引用中的值只能用来找对象,而无法操作内存。这一点正是和指针最大不同,C++中的指针就是一个真实的内存地址,可以通过该地址把内存玩出十八般花样。这点也说明了我们常常把引用传递当成地址传递是错误的(虽说实际效果差不多)。
好吧,看了上边一坨也许你并看不出个什么,也许本身这块有点绕,也许我说的不清楚,那么我们不如直接画图说明上边那个例子到底发生了什么(图示画的不一定和实际完全一致,只为说明问题),说不定你就明白了:tests由于是静态变量,在类加载完就已经实例化了,其在堆内存中分配了长度为3的空间,不过值都为null。在创建t之后,t指向了堆内存中的对象:
tests[0]=t,这就是我们理解错误的地方,这一步test[0]并不是指向t,而是t把Test实例的地址直接赋值给了tests[0],因此tests[0]同样指向Test实例,这和t已经没有任何关系了。
其实从上图我们就应该理解了,t=null之后,其实只是斩断了t和Test实例的关系,并没有改变Test到tests的依赖,从而gc并不会回收Test,这样就造成了逻辑上的内存泄露(为啥说逻辑上,因为明明就是你让tests还存着Test呢,只是你自以为是的以为释放了,当然,这种意义的泄露和C++所说的内存泄露很不同)。
java内存溢出场景:
Java 也是有内存泄露的,虽然Java有著名的GC( 垃圾回收机制),由于Java是通过程序来分配内存空间,释放则交给GC, 由于程序编写不当,仍然会引起内存泄露。
Java中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。例如在Application中保存一个对象,这个对象其实没有使用了,但是由于一直被引用,造成不能回收。
内存泄露的情况是:一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收。
内存泄露的另外一种情况:当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露