内存泄漏
内存泄漏是指不再使用的对象持续占有内存空间而得不到及时释放,从而造成内存空间的浪费称为内存泄漏。所有的内存泄露,最后都会抛出OutOfMemoryError异常。
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。
造成内存泄漏的几种常见情况
1、单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。
如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。
2、集合
典型案例1:一个没有实现hasCode和equals方法的Key类在HashMap中保存的情况。最后会生成很多重复的对象。
/** * HashMap中,由于Key没有实现euqals和hashCode方法,导致可以重入添加,造成内存泄漏。 */ public class KeyWithoutEqualsAndHashCode { public static void main(String[] args) { Map<Person, String> map = new HashMap<Person, String>(1000); while (true) { // creates duplicate objects due to bad Key class Person dummyKey = new Person("zhangsan", 18); //可重复添加,导致内存泄漏 map.put(dummyKey, "value"); } } static class Person { private String name; private int age; Person(String name, int age) { this.name = name; this.age = age; } //省略getter/setter /*@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; if (age != person.age) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; }*/ } }
使用jps查看java进程,找到程序对应的进程pid,再使用jconsole pid号启动jconsole查看监控。
典型案例2:当集合里面的对象属性被修改后,再调用remove()方法不起作用,造成内存泄漏。
/** * 修改了存入Hash结构中的元素的属性,导致hash改变。因此无法再获取到该元素,造成内存泄漏。 */ public class UpdateFieldOfElementInHashSet { public static void main(String[] args) { Set<Person> set = new HashSet<Person>(1000); while (true) { Person dummyPerson = new Person("zhangsan", 18); set.add(dummyPerson); dummyPerson.setAge(28);//修改age属性,导致hash变化 //hash已变,找不到,无法移除 set.remove(dummyPerson); } } } static class Person { private String name; private int age; Person(String name, int age) { this.name = name; this.age = age; } //省略getter/setter @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; if (age != person.age) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } } }
3、静态集合类
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10); for (int i = 1; i<100; i++){ Object o = new Object(); v.add(o); o = null; }
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
4、资源未关闭
比如数据库连接,网络连接(socket)和io连接,除非其显式的调用了其close方法将其连接关闭,否则是不会自动被GC 回收的。
5、线程
锁资源未释放,导致线程泄漏。
6、监视器(Listener)未释放
在释放对象的时候却没有去删除这些监听器,增加了内存泄漏的机会。
7、内部类和外部模块的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: public void registerMsg(Object b); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。