内存泄漏
内存泄漏发生的原因
造成内存泄露的常见情形
内存泄露的解决方案
Java的一个最显著的优势是内存管理。你只需要简单的创建对象而不需要负责释放空间,因为Java的垃圾回收器会负责内存的回收。然而,情况并不是这样简单,内存泄露还是经常会在Java应用程序中出现。
内存泄漏
内存泄露的定义:对于应用程序来说,当对象已经不再被使用,但是Java的垃圾回收器不能回收它们的时候,就产生了内存泄露。
要理解这个定义,我们需要理解对象在内存中的状态。如下图所示,展示了哪些对象是无用对象,哪些是未被引用的对象;
未引用对象将会被垃圾回收器回收,而引用对象却不会。未引用对象很显然是无用的对象。然而,无用的对象并不都是未引用对象,有一些无用对象也有可能是引用对象,这部分对象正是内存泄露的来源。
内存泄漏发生的原因
如下图所示,对象A引用对象B,A的生命周期(t1-t4)比B的生命周期(t2-t3)要长,当B在程序中不再被使用的时候,A仍然引用着B。在这种情况下,垃圾回收器是不会回收B对象的,这就可能造成了内存不足问题,因为A可能不止引用着B对象,还可能引用其它生命周期比A短的对象,这就造成了大量无用对象不能被回收,且占据了昂贵的内存资源。
同样的,B对象也可能引用着一大堆对象,这些被B对象引用着的对象也不能被垃圾回收器回收,所有的这些无用对象消耗了大量内存资源。
造成内存泄露的常见情形
集合类,比如HashMap,ArrayList等,这些对象经常会发生内存泄露。比如当它们被声明为静态对象时,它们的生命周期会跟应用程序的生命周期一样长,很容易造成内存不足。
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10);
for (int i &#61; 1; i<100; i&#43;&#43;)
{
Object o &#61; new Object();
v.add(o);
o &#61; null;
}
当集合里面的对象属性被修改后&#xff0c;再调用remove()方法时不起作用。
package 校招;
import java.util.HashSet;
import java.util.Set;
public class MemoryOut{
public static void main(String[] args){
Set set &#61; new HashSet();
Person p1 &#61; new Person("唐僧","pwd1",25);
Person p2 &#61; new Person("孙悟空","pwd2",26);
Person p3 &#61; new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"&#43;set.size()&#43;" 个元素!"); //结果&#xff1a;总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉&#xff0c;造成内存泄漏
set.add(p3); //重新添加&#xff0c;居然添加成功
System.out.println("总共有:"&#43;set.size()&#43;" 个元素!"); //结果&#xff1a;总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}
}
class Person{
int age;
String name;
String password;
public Person(String name, String password, int age){
this.name &#61; name;
this.password &#61; password;
this.age &#61; age;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age &#61; age;
}
&#64;Override
public int hashCode(){
final int prime &#61; 31;
int result &#61; 1;
result &#61; prime * result &#43; age;
return result;
}
&#64;Override
public boolean equals(Object obj){
if (this &#61;&#61; obj)
return true;
if (obj &#61;&#61; null)
return false;
if (getClass() !&#61; obj.getClass())
return false;
Person other &#61; (Person) obj;
if (age !&#61; other.age)
return false;
return true;
}
}
监听器
在java 编程中&#xff0c;我们都需要和监听器打交道&#xff0c;通常一个应用当中会用到很多监听器&#xff0c;我们会调用一个控件的诸如addXXXListener()等方法来增加监听器&#xff0c;但往往在释放对象的时候却没有记住去删除这些监听器&#xff0c;从而增加了内存泄漏的机会。
各种连接
比如数据库连接(dataSourse.getConnection())&#xff0c;网络连接(socket)和io连接&#xff0c;除非其显式的调用了其close()方法将其连接关闭&#xff0c;否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收&#xff0c;但Connection 一定要显式回收&#xff0c;因为Connection 在任何时候都无法自动回收&#xff0c;而Connection一旦回收&#xff0c;Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池&#xff0c;情况就不一样了&#xff0c;除了要显式地关闭连接&#xff0c;还必须显式地关闭Resultset Statement 对象(关闭其中一个&#xff0c;另外一个也会关闭)&#xff0c;否则就会造成大量的Statement 对象无法释放&#xff0c;从而引起内存泄漏。这种情况下一般都会在try里面去的连接&#xff0c;在finally里面释放连接。
内部类和外部模块的引用
内部类的引用是比较容易遗忘的一种&#xff0c;而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用&#xff0c;例如程序员A 负责A 模块&#xff0c;调用了B 模块的一个方法如&#xff1a;
public void registerMsg(Object b);
这种调用就要非常小心了&#xff0c;传入了一个对象&#xff0c;很可能模块B就保持了对该对象的引用&#xff0c;这时候就需要注意模块B 是否提供相应的操作去除引用。
单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题&#xff0c;单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式)&#xff0c;如果单例对象持有外部的引用&#xff0c;那么这个对象将不能被JVM正常回收&#xff0c;导致内存泄漏&#xff0c;考虑下面的例子&#xff1a;
class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B类采用单例模式
class B{
private A a;
private static B instance&#61;new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a&#61;a;
}
//getter...
}
显然B采用singleton模式&#xff0c;它持有一个A对象的引用&#xff0c;而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况.
内存泄露的解决方案
避免在循环中创建对象。
尽早释放无用对象的引用。 (最基本的建议)
尽量少用静态变量&#xff0c; 因为静态变量存放在永久代(方法区) &#xff0c; 永久代基本不
参与垃圾回收。
使用字符串处理&#xff0c; 避免使用 String&#xff0c; 应大量使用 StringBuffer&#xff0c; 每一个 String
对象都得独立占用内存一块区域。