Java中的内存泄漏

一、什么是内存泄漏

1、内存泄漏的定义

内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间。

在Java中就是指:对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。

最开始以为Java中有垃圾回收机制,所以不会发生内存泄漏,但是通过上面的定义就可以发现

一块内存,如果不再使用后没有被释放,那么就会造成内存泄漏

Java中通过垃圾回收来释放不再使用的内存,而垃圾回收是通过是否有引用指向来判断内存是否不再被使用的。

所以,Java中的内存泄漏,主要造成原因就是程序员以为Java会自动释放所有不再使用的内存,但是Java并不能完全正确地判断内存是否还被继续使用,因此,Java中的内存泄漏,重点关注的就是已经不再使用,但是依然被引用着的对象所占的内存。

2、内存泄漏与内存溢出

  • 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
  • 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak会最终会导致out of memory!

二、内存泄漏的原因

1、使用容器时造成内存泄漏

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

而这一场景,最常见的就是HashMap,ArrayList等集合的使用了。

1
2
3
4
5
6
7
Static Vector v = new Vector(10); 
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}

这是网上最常见的一个例子,当定义了一个Object之后,o指向它,正常来说如果o指向null了,你们这个对象就不再被引用了直接被回收。但问题是这个对象被加入到一个容器之中了,这样就不仅仅是o指向它,容器也有指针指向它,所以回收器不会回收这个对象,而这个容器是static的,生命周期基本与jvm的相同,也就是说它持有的所有对象都不会被回收,所以如果想回收这个对象,一定要调用容器的remove方法。或者在容器不用之后直接向容器赋值未null

2、HashSet的hashcode方法引起的内存泄漏问题

第一种是容器中没有remove造成的泄漏,但是有些时候即使调用了remove方法,也有可能造成内存泄漏,下面也是一个很经典的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

HashSet中,在对应元素添加进set集合后,不要再去修改元素的值,否则对应元素的hashcode值发生变化,此时如果调用
集合的remove(), contains()方法,将不会得到正确的结果。remove()方法并不能正确remove掉对应的元素,造成内存泄漏。

3、JDK1.6中的substring方法造成内存泄漏

虽然没有遇到过,但是据说在早期的String.substring()方法会很容易引起内存泄漏。

原因在于:String内部是char数组,假如有一个很长的大字符串,调用它的substring生成一个小字符串,新的小字符串并不会新建一个char数组,而是直接指向原来的大字符串中的char数组,这样虽然提高了效率并且节省了空间,但是原来的大字符串对象且一直存在,无法被回收。

在JDK1.7之后,修复了这个问题,每次调用substring都会新建一个char数组。

4、各种连接造成的内存泄漏

也可以说是提供了close()方法的对象,只要有

  • 比如数据库连接(dataSourse.getConnection())
  • 网络连接(socket)
  • IO连接

除非其显式的调用了这些对象的close()方法将其连接关闭,否则是不会自动被GC 回收的。

因为在close()方法调用之前,可能会抛出异常而导致close方法没有被调用,所以通常使用try来执行相关逻辑,将close()方法放在finally之中以确保被执行。

5、监听器

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

6、单例模式

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

三、内存泄漏的预防

  1. 使用List、Map等集合时,在使用完成后赋值为null
  2. 一个对象加入set集合后,不要再去修改可能改变其hashcode方法返回值的属性
  3. 目前已知的JDK1.6的substring()方法会导致内存泄露
  4. 及时的关闭打开的文件,socket句柄等
  5. 多关注事件监听(listeners)和回调(callbacks),比如注册了一个listener,当它不再被使用的时候,忘了注销该listener,可能就会产生内存泄露
  6. 使用大对象时,在用完后赋值为null

四、参考地址

http://www.2cto.com/kf/201605/506042.html

http://blog.csdn.net/chenleixing/article/details/43646255

http://blog.csdn.net/seelye/article/details/8269705

http://blog.csdn.net/xidiancyp/article/details/51418158