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

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

什么样的例子?


当前回答

任何时候,只要您保留对不再需要的对象的引用,就会出现内存泄漏。请参阅处理Java程序中的内存泄漏,以了解内存泄漏如何在Java中表现出来以及您可以如何处理它。

其他回答

如果不使用压缩垃圾收集器,则可能会由于堆碎片而发生某种内存泄漏。

可能是潜在内存泄漏以及如何避免它的最简单示例之一,是ArrayList.remove(int)的实现:

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

如果您是自己实现的,您是否想过清除不再使用的数组元素(elementData[-size]=null)?该引用可能会使一个巨大的对象保持活力。。。

关于如何在Java中创建内存泄漏,有很多答案,但请注意采访中提出的问题。

“如何使用Java创建内存泄漏?”是一个开放式问题,其目的是评估开发人员的经验程度。

如果我问你“你有解决Java内存泄漏的经验吗?”,你的答案很简单:“是”。然后,我会继续说“你能给我举个例子来解决内存泄漏问题吗?”,你会给我一两个例子。

然而,当面试官问“如何用Java创建内存泄漏?”时,预期答案应该是以下几行:

我遇到了内存泄漏。。。(说什么时候)[这显示了我的经验]导致它的代码是。。。(解释代码)[你自己修的]我应用的修复基于。。。(解释修复)[这让我有机会询问修复的细节]我做的测试是。。。[让我有机会询问其他测试方法]我是这样记录的。。。[额外加分。如果你记录下来,那就好了]因此,有理由认为,如果我们按照相反的顺序执行,也就是说,得到我修复的代码,然后删除我的修复,我们就会出现内存泄漏。

当开发人员未能遵循这一思路时,我试图引导他/她问“你能给我一个Java如何泄漏内存的例子吗?”,然后问“你曾经修复过Java中的内存泄漏吗?”

请注意,我并不是在询问如何在Java中泄漏内存的示例。那太傻了。谁会对一个能够有效编写泄漏内存的代码的开发人员感兴趣?

这里的大多数例子都“过于复杂”。它们是边缘案例。在这些例子中,程序员犯了一个错误(比如不要重新定义equals/hashcode),或者被JVM/JAVA的一个极端情况(用静态加载类…)所咬。我认为这不是面试官想要的例子,甚至不是最常见的例子。

但内存泄漏的情况确实更简单。垃圾收集器只释放不再引用的内容。我们作为Java开发人员并不关心内存。我们在需要时分配它,并让它自动释放。好的

但任何长寿命的应用程序都倾向于共享状态。它可以是任何东西,静态的,单态的。。。通常,非平凡的应用程序倾向于生成复杂的对象图。只是忘记将引用设置为null,或者更经常地忘记从集合中删除一个对象,就足以造成内存泄漏。

当然,如果处理不当,所有类型的侦听器(如UI侦听器)、缓存或任何长期共享状态都会产生内存泄漏。应该理解的是,这不是Java角落的情况,也不是垃圾收集器的问题。这是一个设计问题。我们设计为向长寿命对象添加侦听器,但在不再需要时不删除侦听器。我们缓存对象,但我们没有从缓存中删除它们的策略。

我们可能有一个复杂的图来存储计算所需的先前状态。但前一状态本身与前一状态相关联,依此类推。

就像我们必须关闭SQL连接或文件一样。我们需要设置对null的正确引用,并从集合中删除元素。我们应该有适当的缓存策略(最大内存大小、元素数量或计时器)。所有允许通知侦听器的对象必须同时提供addListener和removeListener方法。当这些通知器不再使用时,它们必须清除侦听器列表。

内存泄漏确实是可能的,而且完全可以预测。无需特殊的语言功能或角盒。内存泄漏要么是某些东西可能丢失的指示,甚至是设计问题。

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

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

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

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

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

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

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

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

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