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

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

什么样的例子?


当前回答

从finalize方法引发未处理的异常。

其他回答

不终止的线程(比如在其运行方法中无限期休眠)。即使我们丢失了对它的引用,它也不会被垃圾收集。您可以添加字段以使线程对象变大。

目前排名靠前的答案列出了更多的技巧,但这些似乎是多余的。

这是一个简单/险恶的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实例被丢弃之后也是如此。

JDK 1.7之前内存泄漏的实时示例:

假设您读取了一个包含1000行文本的文件,并将其保存在String对象中:

String fileText = 1000 characters from file
fileText = fileText.subString(900, fileText.length());

在上面的代码中,我最初读取了1000个字符,然后执行了子字符串,只获得最后100个字符。现在,fileText应该只引用100个字符,所有其他字符都应该被垃圾收集,因为我丢失了引用,但是在JDK1.7之前,substring函数间接引用了最后100个字符的原始字符串,并阻止了整个字符串的垃圾收集,而整个1000个字符将一直保存在内存中,直到您丢失了对子字符串的引用。

您可以创建一个类似于上述的内存泄漏示例。

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中不存在内存泄漏。内存泄漏是从C等人那里借来的一个短语。Java借助GC在内部处理内存分配。存在内存浪费(即留下滞留对象),但没有内存泄漏。