让我们仔细看看其中一些场景以及如何处理它们。 Java中的内存泄漏类型 在任何应用程序中,由于多种原因都可能发生内存泄漏: 1. 静态字段 可能导致潜在内存泄漏的第一种情况是大量使用静态变量。 在Java中,静态字段的生命周期通常与正在运行的应用程序的整个生命周期相匹配(除非ClassLoader符合垃圾回收的条件)。 让我们创建一个填充静态 List的简单Java程序 :
public class StaticTest {
public static List<Double> list = new ArrayList<>();
public void populateList() {
for (int i = 0; i < 10000000; i++) {
list.add(Math.random());
}
Log.info("Debug Point 2");
}
public static void main(String[] args) {
Log.info("Debug Point 1");
new StaticTest().populateList();
Log.info("Debug Point 3");
}
}
2. 未关闭的连接池资源 每当我们建立新连接或打开流时,JVM都会为这些资源分配内存。一些示例包括数据库连接,输入流和会话对象。 忘记关闭这些资源可以阻止内存,从而使它们远离GC的范围。如果异常阻止程序执行到达处理代码以关闭这些资源的语句,则甚至可能发生这种情况。
3. 不正确的equals()和hashCode()实现 在定义新类时,一个非常常见的疏忽是不为equals()和hashCode()方法编写适当的重写方法。 HashSet 和 HashMap 在许多操作中使用这些方法,如果它们没有被正确覆盖,那么它们可能成为潜在的内存泄漏问题的来源。 让我们以一个简单的Person 类为例, 并将其用作HashMap中的键 :
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
}
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map<Person, Integer> map = new HashMap<>();
for(int i=0; i<100; i++) {
map.put(new Person("jon"), 1);
}
Assert.assertFalse(map.size() == 1);
}
如果我们正确地重写了 equals() 和hashCode()方法,那么在这个Map中只会存在一个Person对象。让我们一起来看看正确实现的equals()和hashCode()方法:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Person)) {
return false;
}
Person person = (Person) o;
return person.name.equals(name);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
return result;
}
}
在这种情况下,以下断言将成立:
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
Map<Person, Integer> map = new HashMap<>();
for(int i=0; i<2; i++) {
map.put(new Person("jon"), 1);
}
Assert.assertTrue(map.size() == 1);
}
4.引用外类的内部类 这种情况发生在非静态内部类(匿名类)的情况下。对于初始化,这些内部类总是需要封闭类的实例。 默认情况下,每个非静态内部类都包含对其包含类的隐式引用。如果我们在应用程序中使用这个内部类'对象,那么即使在我们的包含类'对象超出范围之后,它也不会被垃圾收集。 因为内部类对象隐式地保存对外部类对象的引用,从而使其成为垃圾收集的无效候选者。在匿名类的情况下也是如此。 如何预防呢?
5. finalize()方法 是潜在的内存泄漏问题的另一个来源。每当重写类的 finalize()方法时,该类的对象不会立即被垃圾收集。相反,GC将它们排队等待最终确定,在稍后的时间点才会发送GC。 如果用finalize()方法编写的代码不是最佳的,并且finalize队列无法跟上Java垃圾收集器,那么迟早,我们的应用程序注定要遇到 OutOfMemoryError。 如何预防呢?
6. 内部字符串 Java 7的重大变化:Java String池在从PermGen转移到HeapSpace了。但是对于在版本6及更低版本上运行的应用程序,在使用大型字符串时我们应该更加专心。 如果我们读取一个庞大的大量String对象,并在该对象上调用intern(),那么它将转到字符串池,它位于PermGen(永久内存)中,并且只要我们的应用程序运行就会保留在那里。这会阻止内存收集并在我们的应用程序中造成重大内存泄漏。 如何预防呢?
7. 使用ThreadLocal ThreadLocal使我们能够将状态隔离到特定线程,从而允许我们实现线程安全。 使用此构造时, 每个线程将保留对其ThreadLocal变量副本的隐式引用,并且将保留其自己的副本,而不是跨多个线程共享资源,只要该线程处于活动状态即可。 尽管有其优点,ThreadLocal 变量的使用仍存在争议,因为如果使用不当,它们会因引入内存泄漏而臭名昭着。Joshua Bloch 曾评论线程本地用法:
try {
threadLocal.set(System.nanoTime());
//... further processing
}
finally {
threadLocal.remove();
}