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

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

什么样的例子?


当前回答

我认为,一个有效的例子可能是在线程集中的环境中使用ThreadLocal变量。

例如,使用Servlet中的ThreadLocal变量与其他web组件通信,让容器创建线程,并在池中维护空闲线程。ThreadLocal变量如果没有正确清理,将一直存在,直到同一个web组件覆盖它们的值。

当然,一旦确定,问题很容易解决。

其他回答

Swing使用对话框非常简单。创建一个JDialog,显示它,用户关闭它,然后泄漏!

您必须调用dispose()或配置setDefaultCloseOperation(dispose_ON_CLOSE)。

这里有一个在纯Java中创建真正的内存泄漏(运行代码无法访问但仍存储在内存中的对象)的好方法:

应用程序创建一个长时间运行的线程(或者使用线程池更快地泄漏)。线程通过(可选的自定义)ClassLoader加载类。该类分配一大块内存(例如新字节[10000000]),在静态字段中存储对它的强引用,然后在ThreadLocal中存储对自身的引用。分配额外的内存是可选的(泄漏类实例就足够了),但这会使泄漏工作得更快。应用程序清除对自定义类或从中加载该类的ClassLoader的所有引用。重复

由于ThreadLocal在Oracle的JDK中的实现方式,这会造成内存泄漏:

每个线程都有一个私有字段threadLocals,它实际上存储线程本地值。此映射中的每个键都是对ThreadLocal对象的弱引用,因此在ThreadLocal对象被垃圾收集后,其条目将从映射中删除。但每个值都是一个强引用,因此当一个值(直接或间接)指向作为其键的ThreadLocal对象时,只要线程存在,该对象既不会被垃圾收集,也不会从映射中删除。

在本例中,强引用链如下所示:

线程对象→ threadLocals映射→ 示例类的实例→ 示例类→ 静态ThreadLocal字段→ ThreadLocal对象。

(ClassLoader在创建泄漏时并没有真正发挥作用,它只是因为这个额外的引用链而使泄漏变得更糟:example类→ 类加载器→ 它加载的所有类。在许多JVM实现中,尤其是在Java7之前,情况更糟,因为类和ClassLoader被直接分配到permagen中,根本不会被垃圾收集。)

这种模式的一个变体是,如果您经常重新部署碰巧使用ThreadLocal的应用程序,而这些应用程序在某种程度上指向自己,那么应用程序容器(如Tomcat)会像筛子一样泄漏内存。这种情况可能有许多微妙的原因,并且通常很难调试和/或修复。

更新:由于很多人一直在要求它,这里有一些示例代码显示了这种行为。

答案完全取决于面试官认为他们在问什么。

在实践中是否可能造成Java泄漏?当然是这样,其他答案中有很多例子。

但有很多元问题可能被问到了?

理论上“完美”的Java实现是否容易泄漏?候选人是否理解理论与现实之间的区别?应聘者是否了解垃圾收集的工作原理?或者垃圾收集在理想情况下应该如何工作?他们知道他们可以通过本地接口调用其他语言吗?他们知道用其他语言泄露内存吗?应聘者是否知道什么是内存管理,以及Java的幕后情况?

我把你的元问题理解为“在这种面试情况下我可以用什么答案”。因此,我将重点关注面试技巧,而不是Java。我相信,你更可能重复在面试中不知道问题答案的情况,而不是你需要知道如何使Java泄漏。所以,希望这会有所帮助。

你可以培养的面试最重要的技能之一是学会积极倾听问题,并与面试官合作以提取他们的意图。这不仅可以让你以他们想要的方式回答他们的问题,还表明你有一些重要的沟通技巧。当要在许多同样有才华的开发人员之间做出选择时,我会雇佣一个在他们每次回应之前都能倾听、思考和理解的人。

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);
}

我认为,一个有效的例子可能是在线程集中的环境中使用ThreadLocal变量。

例如,使用Servlet中的ThreadLocal变量与其他web组件通信,让容器创建线程,并在池中维护空闲线程。ThreadLocal变量如果没有正确清理,将一直存在,直到同一个web组件覆盖它们的值。

当然,一旦确定,问题很容易解决。