原文:Understanding Memory Leaks in Java
内存泄漏是指不再使用的对象持续占有内存空间而得不到及时释放,从而造成内存空间的浪费称为内存泄漏。比如,长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。所有的内存泄露,最后都会抛出OutOfMemoryError异常。
一、内存泄漏的常见情况
1.static字段引起的内存泄漏
可能导致潜在内存泄漏的第一种情况是大量使用static变量。在Java中,静态字段通常拥有与整个应用程序相匹配的生命周期(除非ClassLoader符合垃圾回收的条件)。
来看下面这段程序:
public class StaticTest { public static List<Double> list = new ArrayList<>(); public void populateList() { for (int i = 0; i < 10000000; i++) { list.add(Math.random()); } } public static void main(String[] args) {new StaticTest().populateList(); } }
运行前打开visualvm, 然后运行程序,visualvm就会进行监控到运行程序的进程。
如何预防呢?
- 最大限度地减少静态变量的使用
- 使用单例时,依赖于延迟加载对象而不是立即加载的方式
2.资源未关闭导致内存泄漏
①数据库连接,网络连接(socket)和io连接,需要显式的调用close方法将其连接关闭,否则是不会自动被GC 回收的。
②锁资源未释放,导致线程泄漏。比如ReentrantLock用完,记得用lock.unlock()来释放锁。
如何预防呢?
- 始终使用finally块来关闭资源
- 关闭资源的代码(甚至在 finally块中)本身不应该有任何异常
- 使用Java 7+时,我们可以使用try -with-resources块
3.hashcode数据结构产生的内存泄漏
典型案例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; } } }
如何预防呢?
- 根据经验,定义新的实体时,总要重写equals()和hashCode()方法。
- 只是重写他们是不够的,这些方法必须以最佳的方式被重写。
4.ThreadLocal
ThreadLocal用完一定要remove,否则可能会造成内存泄漏。可以参考博客:ThreadLocal源码分析
5.监视器(Listener)未释放
web开发中经常会用到监视器(Listener),但在释放对象的时候却没有去删除这些监听器,增加了内存泄漏的机会。
6.内部类和外部模块的引用
这种情况发生在非静态内部类(匿名类)的情况下。对于初始化,这些内部类总是需要外部类的实例。
默认情况下,每个非静态内部类都包含对其包含类的隐式引用。如果我们在应用程序中使用这个内部类对象,那么即使在我们的包含类对象超出范围之后,它也不会被垃圾收集。
如何预防呢?
- 如果内部类不需要访问包含的类成员,可以考虑将其转换为静态类