我刚刚接受了一次采访,被要求用Java创建内存泄漏。

不用说,我觉得自己很傻,不知道如何开始创作。

什么样的例子?


当前回答

我想就如何使用JVM中可用的工具监视应用程序的内存泄漏提供建议。它没有显示如何生成内存泄漏,但解释了如何使用最少的可用工具检测内存泄漏。

您需要首先监视Java内存消耗。

最简单的方法是使用JVM附带的jstat实用程序:

jstat -gcutil <process_id> <timeout>

它将报告每一代(年轻、老年和老年)的内存消耗和垃圾收集时间(年轻和完整)。

一旦您发现一个完整的垃圾收集执行得太频繁并且花费了太多时间,您就可以假设应用程序正在泄漏内存。

然后需要使用jmap实用程序创建内存转储:

jmap -dump:live,format=b,file=heap.bin <process_id>

然后需要使用内存分析器(例如Eclipse memory Analyzer(MAT))分析heap.bin文件。

MAT将分析内存并向您提供有关内存泄漏的可疑信息。

其他回答

如果您不了解JDBC,下面是一个毫无意义的示例。或者至少是JDBC希望开发人员在丢弃Connection、Statement和ResultSet实例或丢失对它们的引用之前关闭它们,而不是依赖于实现finalize方法。

void doWork() {
    try {
        Connection conn = ConnectionFactory.getConnection();
        PreparedStatement stmt = conn.preparedStatement("some query");
        // executes a valid query
        ResultSet rs = stmt.executeQuery();
        while(rs.hasNext()) {
            // ... process the result set
        }
    } catch(SQLException sqlEx) {
        log(sqlEx);
    }
}

上面的问题是Connection对象没有关闭,因此物理Connection将保持打开状态,直到垃圾回收器返回并发现它不可访问为止。GC将调用finalize方法,但有些JDBC驱动程序没有实现finalize,至少与Connection.close的实现方式不同。由此产生的行为是,尽管JVM将由于收集不可访问的对象而回收内存,但与Connection对象关联的资源(包括内存)可能不会被回收。

因此,Connection的最终方法并不能清除所有内容。人们可能会发现,到数据库服务器的物理连接将持续几个垃圾收集周期,直到数据库服务器最终发现该连接不活动(如果存在),应该关闭。

即使JDBC驱动程序实现了finalize,编译器也可以在finalize期间抛出异常。由此产生的行为是,与现在“休眠”对象关联的任何内存都不会被编译器回收,因为finalize保证只被调用一次。

上述在对象完成过程中遇到异常的场景与另一种可能导致内存泄漏的场景有关——对象复活。对象复活通常是通过创建一个从另一个对象最终确定的对象的强引用来实现的。当对象复活被误用时,它将与其他内存泄漏源一起导致内存泄漏。

还有很多例子你可以想象出来

管理列表实例,其中您只添加到列表中,而不从列表中删除(尽管您应该删除不再需要的元素),或者打开套接字或文件,但不再需要时不关闭它们(类似于上面涉及Connection类的示例)。在关闭Java EE应用程序时不卸载Singleton。加载单例类的Classloader将保留对该类的引用,因此JVM永远不会收集单例实例。当部署应用程序的新实例时,通常会创建一个新的类加载器,而由于单例,前一个类加载器将继续存在。

保存对象引用的静态字段(尤其是最终字段)

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

(未关闭)开放流(文件、网络等)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStackTrace();
}

未封闭的连接

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStackTrace();
}

JVM垃圾收集器无法访问的区域,例如通过本机方法分配的内存。

在web应用程序中,某些对象存储在应用程序范围中,直到应用程序被显式停止或删除。

getServletContext().setAttribute("SOME_MAP", map);

不正确或不适当的JVM选项,例如IBM JDK上的noclassgc选项,它阻止未使用的类垃圾收集

请参阅IBM JDK设置。

这是一个简单/险恶的http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29.

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

因为子字符串指的是原始字符串的内部表示,所以原始字符串会保留在内存中。因此,只要你有一个StringLeaker在玩,你的记忆中也有整个原始字符串,即使你可能认为你只是在保存一个字符串。

避免存储对原始字符串的不需要的引用的方法如下:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

为了增加坏处,您还可以.intern()子字符串:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

这样做将在内存中保留原始的长字符串和派生的子字符串,即使在StringLeaker实例被丢弃之后也是如此。

Java中有很多内存泄漏的好例子,我将在这个答案中提到其中两个。

示例1:

以下是《有效Java,第三版》(第7项:消除过时的对象引用)一书中的一个内存泄漏的好例子:

// Can you spot the "memory leak"?
public class Stack {
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    private Object[] elements;
    private int size = 0;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0) throw new EmptyStackException();
        return elements[--size];
    }

    /*** Ensure space for at least one more element, roughly* doubling the capacity each time the array needs to grow.*/
    private void ensureCapacity() {
        if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

本书的这一段描述了为什么此实现会导致内存泄漏:

如果堆栈增长然后收缩即使程序使用堆栈没有对它们的更多引用。这是因为堆栈维护对这些对象的过时引用。一个过时的引用只是一个永远不会被取消引用的引用再一次在这种情况下元素数组已过时。活动部分包括索引小于大小的元素

以下是本书解决此内存泄漏的解决方案:

解决这类问题的方法很简单:null out引用一旦过时。在Stack类的情况下,对项目的引用一经弹出就过时从堆栈中删除。pop方法的修正版本如下所示:

public Object pop() {
    if (size == 0) throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

但我们如何防止内存泄漏的发生?这是本书中一个很好的警告:

一般来说,每当类管理自己的内存时,程序员应该警惕内存泄漏。每当元素元素中包含的任何对象引用都应该为空。

示例2:

观察者模式也会导致内存泄漏。您可以在以下链接中阅读此模式:观察者模式。

这是观察者模式的一种实现:

class EventSource {
    public interface Observer {
        void update(String event);
    }

    private final List<Observer> observers = new ArrayList<>();

    private void notifyObservers(String event) {
        observers.forEach(observer -> observer.update(event)); //alternative lambda expression: observers.forEach(Observer::update);
    }

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void scanSystemIn() {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            notifyObservers(line);
        }
    }
}

在这个实现中,EventSource(在Observer设计模式中是可观察的)可以保存到Observer对象的链接,但这个链接从未从EventSource的Observer字段中删除。所以垃圾收集器永远不会收集它们。解决这一问题的一个解决方案是向客户提供另一种方法,当他们不再需要这些观察员时,将上述观察员从观察员字段中删除:

public void removeObserver(Observer observer) {
    observers.remove(observer);
}

我可以从这里复制我的答案:在Java中导致内存泄漏的最简单方法

“在计算机科学中,当计算机程序消耗内存但无法将其释放回操作系统时,就会发生内存泄漏。”(维基百科)

简单的答案是:你不能。Java执行自动内存管理,并将释放您不需要的资源。你无法阻止这种情况的发生。它将始终能够释放资源。在具有手动内存管理的程序中,这是不同的。可以使用malloc()在C中获得一些内存。要释放内存,您需要malloc返回的指针并对其调用free()。但是,如果您不再拥有指针(被覆盖或超过生存期),那么很遗憾,您无法释放此内存,因此会出现内存泄漏。

到目前为止,所有其他答案在我的定义中都不是真正的内存泄漏。他们的目标都是快速用毫无意义的东西填满记忆。但在任何时候,您仍然可以取消引用创建的对象,从而释放内存-->无泄漏。尽管我不得不承认,acconrad的答案非常接近,因为他的解决方案实际上是通过强制垃圾收集器进入一个无休止的循环来“崩溃”垃圾收集器)。

长时间的答案是:通过使用JNI为Java编写库,可以获得内存泄漏,JNI可以进行手动内存管理,从而产生内存泄漏。如果调用此库,Java进程将泄漏内存。或者,JVM中可能存在bug,从而导致JVM丢失内存。JVM中可能存在bug,甚至可能存在一些已知的bug,因为垃圾收集并不是那么简单,但它仍然是一个bug。根据设计,这是不可能的。您可能需要一些受此类错误影响的Java代码。很抱歉,我不知道,而且在下一个Java版本中,它可能不再是一个bug。