如何在 Java 中创建内存泄漏?

Posted

技术标签:

【中文标题】如何在 Java 中创建内存泄漏?【英文标题】:How can I create a memory leak in Java? 【发布时间】:2011-09-22 04:12:08 【问题描述】:

我刚刚接受了一次面试,我被要求用 Java 创建一个内存泄漏

不用说,我什至不知道如何开始创建一个,感觉很愚蠢。

有什么例子?

【问题讨论】:

具有讽刺意味的是,对于每个重要的 Java 程序来说,更难的问题是如何造成内存泄漏! 继续向容器中添加新对象,但忘记添加删除它们的代码或实现部分工作的代码,这些代码不会随着程序的进行而清除所有对象。 【参考方案1】:

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

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

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

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

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

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

(ClassLoader 并没有真正起到造成泄漏的作用,它只是因为这个额外的引用链而使泄漏变得更糟:示例类 → ClassLoader → 它已加载的所有类。甚至在许多 JVM 实现中更糟糕,尤其是在 Java 7 之前,因为类和 ClassLoaders 被直接分配到 permgen 中,根本没有被垃圾回收。)

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

更新:由于很多人一直在要求它,here's some example code that shows this behavior in action。

【讨论】:

+1 ClassLoader 泄漏是 JEE 世界中最常见的一些内存泄漏,通常由转换数据的第三方库(BeanUtils、XML/JSON 编解码器)引起。当 lib 在应用程序的根类加载器之外加载但包含对您的类的引用(例如,通过缓存)时,可能会发生这种情况。当您取消部署/重新部署应用程序时,JVM 无法垃圾收集应用程序的类加载器(因此它加载的所有类),因此重复部署应用程序服务器最终会失败。如果幸运的话,您会发现 ClassCastException z.x.y.Abc cannot be cast to z.x.y.Abc +1:类加载器泄漏是一场噩梦。我花了数周时间试图弄清楚它们。可悲的是,正如@earcam 所说,它们主要是由 3rd 方库引起的,而且大多数分析器都无法检测到这些泄漏。这个博客上有一个关于类加载器泄漏的很好而清晰的解释。 blogs.oracle.com/fkieviet/entry/…【参考方案2】:

包含对象引用的静态字段[尤其是 final field]

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

在长字符串上调用String.intern()

String str = readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

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

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 settings。

【讨论】:

我不同意上下文和会话属性是“泄漏”。它们只是长期存在的变量。静态最终字段或多或少只是一个常数。也许应该避免使用大的常量,但我认为将其称为内存泄漏是不公平的。 (未关闭)打开的流(文件、网络等...),在最终确定期间(将在下一个 GC 周期之后)关闭时不会真正泄漏() 将被调度(close() 通常不会在终结器线程中调用,因为可能是阻塞操作)。不关闭是不好的做法,但不会导致泄漏。未关闭的java.sql.Connection也是一样的。 在大多数正常的 JVM 中,似乎 String 类在其 intern 哈希表内容上仅具有弱引用。因此,它正确收集垃圾而不是泄漏。 (但 IANAJP)mindprod.com/jgloss/interned.html#GC【参考方案3】:

一个简单的做法是使用不正确(或不存在)hashCode()equals() 的 HashSet,然后继续添加“重复项”。集合不会像它应该忽略的那样忽略重复项,而是只会增长并且您将无法删除它们。

如果你想让这些坏键/元素到处乱跑,你可以使用像

这样的静态字段
class BadKey 
   // no hashCode or equals();
   public final String key;
   public BadKey(String key)  this.key = key; 


Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

【讨论】:

其实,即使元素类得到hashCode和equals错误,你也可以从HashSet中移除元素;只需获取集合的迭代器并使用它的 remove 方法,因为迭代器实际上对底层条目本身而不是元​​素进行操作。 (请注意,未实现的 hashCode/equals 不足以触发泄漏;默认实现简单的对象标识,因此您可以获取元素并正常删除它们。) @Donal 你最后的陈述不正确。您无法获取该元素,因为您没有引用它。对放在地图中的键的唯一引用是地图本身持有的那个。只有你去了BadKey myKey = new BadKey("key"); map.put(key,"value");,你才能把它弄出来。你是对的,你可以使用迭代器来删除它,但你不能总是对所有数据结构都这样做。例如,如果您不取消@meriton 答案中的引用,那将永远丢失。你无权访问后备数组,迭代器将停止它。 在 equals/hashCode 不正确时删除元素的唯一方法是在使用不同的比较方法找到匹配项时使用 Iterator.remove()。 @Donal 我想我想说的是,我不同意您对内存泄漏的定义。我会考虑(继续类比)您的迭代器删除技术是泄漏下的滴水盘;无论滴盘如何,泄漏仍然存在。 我同意,这不是内存“泄漏”,因为您可以删除对哈希集的引用并等待 GC 启动,然后就可以了!记忆回到过去。【参考方案4】:

下面会出现一个不明显的 Java 泄漏案例,除了标准案例被遗忘的侦听器、静态引用、hashmap 中的虚假/可修改键,或者只是线程卡住而没有任何机会结束其生命周期。

File.deleteOnExit() - 总是泄漏字符串,如果字符串是子字符串,泄漏会更严重(底层的 char[] 也会泄漏) - 在 Java 7 中,子字符串也会复制 @ 987654322@,所以后者不适用; @Daniel,不过,不需要投票。

我将集中在线程上,主要展示非托管线程的危险,甚至不想碰摇摆。

Runtime.addShutdownHook 并且不删除...,然后即使使用 removeShutdownHook 由于 ThreadGroup 类中关于未启动线程的错误,它可能不会被收集,有效地泄漏 ThreadGroup。 JGroup 在 GossipRouter 中有漏洞。

创建但未启动 Thread 属于与上述相同的类别。

创建线程会继承ContextClassLoaderAccessControlContext,加上ThreadGroup 和任何InheritedThreadLocal,所有这些引用以及类加载器加载的整个类和所有静态引用都是潜在的泄漏, 和 ja-ja。整个 j.u.c.Executor 框架的效果尤其明显,该框架具有超级简单的ThreadFactory 接口,但大多数开发人员对潜伏的危险一无所知。还有很多库会根据请求启动线程(行业流行的库太多了)。

ThreadLocal 缓存;在许多情况下,这些都是邪恶的。我相信每个人都看过很多基于 ThreadLocal 的简单缓存,但坏消息是:如果线程持续运行超过预期的上下文 ClassLoader 的生命周期,这纯粹是一个不错的小泄漏。除非确实需要,否则不要使用 ThreadLocal 缓存。

当线程组本身没有线程时调用ThreadGroup.destroy(),但它仍然保留子线程组。严重的泄漏会阻止 ThreadGroup 从其父级中删除,但所有子级都变得不可枚举。

使用 Wea​​kHashMap 和值 (in) 直接引用键。如果没有堆转储,这很难找到。这适用于所有可能保留对受保护对象的硬引用的扩展 Weak/SoftReference

java.net.URL 与 HTTP(S) 协议一起使用并从 (!) 加载资源。这个很特别,KeepAliveCache 在系统 ThreadGroup 中创建了一个新线程,它会泄露当前线程的上下文类加载器。当没有活动线程存在时,该线程是在第一次请求时创建的,因此您可能会幸运或只是泄漏。 泄漏已经在 J​​ava 7 中得到修复,创建线程的代码会正确删除上下文类加载器。还有少数情况(像 ImageFetcher也已修复 em>) 创建类似的线程。

使用InflaterInputStream 在构造函数中传递new java.util.zip.Inflater()(例如PNGImageDecoder)而不是调用充气器的end()。好吧,如果你只用new 传入构造函数,就没有机会......而且是的,如果它作为构造函数参数手动传递,则在流上调用close() 不会关闭充气器。这不是真正的泄漏,因为它会被终结器释放......当它认为有必要时。直到那一刻,它严重地吃掉了本机内存,它可能导致 Linux oom_killer 杀死进程而不受惩罚。主要问题是 Java 中的最终确定非常不可靠,G1 使情况变得更糟,直到 7.0.2。故事的寓意:尽快释放原生资源;终结器太差了。

java.util.zip.Deflater 的情况相同。这要糟糕得多,因为 Deflater 在 Java 中很耗内存,即总是使用 15 位(最大)和 8 级内存(最大 9 级)来分配数百 KB 的本机内存。幸运的是,Deflater 并没有被广泛使用,据我所知,JDK 没有滥用。如果您手动创建DeflaterInflater,请始终调用end()。最后两个中最好的部分:您无法通过可用的常规分析工具找到它们。

(我可以根据要求添加更多我遇到的浪费时间。)

祝你好运,保持安全;泄漏是邪恶的!

【讨论】:

Creating but not starting a Thread... 哎呀,几个世纪前我被这个咬得很厉害! (Java 1.3) @leonbloy,在线程被直接添加到线程组之前更糟,不启动意味着非常严重的泄漏。它不仅增加了unstarted 计数,而且防止线程组被破坏(较小的邪恶但仍然是泄漏) 谢谢! “当 ThreadGroup 本身没有线程时调用 ThreadGroup.destroy()...” 是一个非常微妙的错误;我已经追了几个小时了,误入歧途,因为在我的控制 GUI 中枚举线程没有显示任何内容,但是线程组和大概至少一个子组不会消失。 @bestsss :我很好奇,既然它在 JVM 关闭时运行,为什么要删除关闭挂钩? @user253751 :这是在内存泄漏的情况下。在这种情况下,在使用关闭挂钩后未能删除它并不是泄漏,因为 JVM 正在关闭。当然,如果你只是暂时需要在关机时做一些事情,那么不删除它并不断添加新的处理程序会泄漏。【参考方案5】:

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

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

但任何长期存在的应用程序都倾向于具有共享状态。它可以是任何东西,静态的,单例的......通常不平凡的应用程序倾向于制作复杂的对象图。只是忘记设置对 null 的引用或更经常忘记从集合中删除一个对象就足以造成内存泄漏。

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

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

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

内存泄漏确实是可能的,并且完全可以预测。不需要特殊的语言功能或极端情况。内存泄漏要么表明可能缺少某些东西,要么表明存在设计问题。

【讨论】:

我觉得有趣的是,在其他答案中人们正在寻找那些边缘案例和技巧,并且似乎完全没有抓住重点。他们可以只显示代码,保留对永远不会再次使用的对象的无用引用,并且永远不会删除这些引用;有人可能会说这些情况不是“真正的”内存泄漏,因为周围仍然存在对这些对象的引用,但是如果程序不再使用这些引用并且也永远不会删除它们,那么它完全等同于(和一样糟糕)“真正的内存泄漏”。【参考方案6】:

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

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

但是可能已经提出了多个元问题?

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

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

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

【讨论】:

每当我问这个问题时,我都在寻找一个非常简单的答案 - 不断增加队列,没有最终关闭数据库等,没有奇怪的类加载器/线程细节,这意味着他们了解 gc 可以和不能为你做。我猜这取决于你面试的工作。【参考方案7】:

如果您不了解JDBC,以下是一个毫无意义的示例。或者至少 JDBC 期望开发人员在丢弃它们或丢失对它们的引用之前关闭 ConnectionStatementResultSet 实例,而不是依赖于 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 对象没有关闭,因此物理连接将保持打开状态,直到垃圾收集器发现它无法访问。 GC 会调用finalize 方法,但是有些JDBC 驱动程序没有实现finalize,至少与实现Connection.close 的方式不同。由此产生的行为是,虽然由于收集了无法访问的对象而回收了内存,但与 Connection 对象关联的资源(包括内存)可能根本不会被回收。

Connectionfinalize 方法无法清理所有内容的情况下,实际上可能会发现与数据库服务器的物理连接将持续几个垃圾回收周期,直到数据库服务器最终弄清楚连接不存在(如果存在),应该关闭。

即使 JDBC 驱动程序要实现 finalize,也有可能在终结期间引发异常。结果行为是任​​何与现在“休眠”对象关联的内存都不会被回收,因为finalize 保证只会被调用一次。

上述在对象终结期间遇到异常的场景与另一个可能导致内存泄漏的场景有关 - 对象复活。对象复活通常是通过从另一个对象创建对对象的强引用来有意完成的。当对象复活被滥用时,它会导致内存泄漏与其他内存泄漏源相结合。

还有很多你可以想出的例子——比如

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

【讨论】:

您通常会在达到内存限制之前达到最大打开连接限制。不要问我为什么知道...【参考方案8】:

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)?该引用可能会使一个巨大的对象保持活力......

【讨论】:

内存泄漏在哪里? @maniek:我并不是要暗示这段代码存在内存泄漏。我引用它来表明有时需要不明显的代码来避免意外的对象保留。【参考方案9】:

任何时候,如果您保留对不再需要的对象的引用,就会发生内存泄漏。请参阅Handling memory leaks in Java programs,了解内存泄漏在 Java 中的表现方式以及您可以采取的措施。

【讨论】:

@31eee384 让我们continue this discussion in chat【参考方案10】:

您可以使用 sun.misc.Unsafe 类造成内存泄漏。事实上,这个服务类在不同的标准类中使用(例如在 java.nio 类中)。 您不能直接创建此类的实例,但您可以使用反射来做到这一点

代码无法在 Eclipse IDE 中编译 - 使用命令 javac 编译它(编译期间您会收到警告)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe 

    public static void main(String[] args) throws Exception
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        
            for(;;)
                unsafe.allocateMemory(1024*1024);
         catch(Error e) 
            System.out.println("Boom :)");
            e.printStackTrace();
        
    

【讨论】:

【参考方案11】:

我可以从这里复制我的答案: Easiest way to cause memory leak in Java

“计算机科学中的内存泄漏(或在此上下文中的泄漏)发生在计算机程序消耗内存但无法将其释放回操作系统时。” (***)

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

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

长答案是:您可以通过使用 JNI 为 Java 编写一个库来获得内存泄漏,该库可以进行手动内存管理,因此存在内存泄漏。如果你调用这个库,你的 Java 进程会泄漏内存。或者,您可能在 JVM 中存在错误,从而导致 JVM 丢失内存。 JVM 中可能存在错误,甚至可能有一些已知的错误,因为垃圾收集并不是那么微不足道,但它仍然是一个错误。按照设计,这是不可能的。您可能会要求一些受此类错误影响的 Java 代码。抱歉,我不知道,在下一个 Java 版本中它可能不再是一个错误。

【讨论】:

这是一个非常有限(而且不是很有用)的内存泄漏定义。对于实际目的而言,唯一有意义的定义是“内存泄漏是指程序在不再需要它所保存的数据之后继续持有分配的内存的任何情况。”【参考方案12】:

通过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 实例被丢弃后,这样做也会将原始长字符串和派生子字符串保留在内存中。

【讨论】:

方法substring()在java7中创建了一个新的String(这是一个新的行为)【参考方案13】:

GUI 代码中的一个常见示例是创建小部件/组件并向某些静态/应用程序范围的对象添加侦听器,然后在销毁小部件时不移除侦听器。您不仅会遇到内存泄漏,还会影响性能,因为当您正在监听的任何事件触发事件时,所有旧监听器也会被调用。

【讨论】:

【参考方案14】:

也许通过 JNI 使用外部本机代码?

使用纯 Java 几乎是不可能的。

但这是关于“标准”类型的内存泄漏,当您无法再访问内存时,它仍然归应用程序所有。您可以改为保留对未使用对象的引用,或者打开流而不在之后关闭它们。

【讨论】:

这取决于“内存泄漏”的定义。如果“保留但不再需要的内存”,那么在 Java 中很容易做到。如果它是“已分配但代码根本无法访问的内存”,那么它会稍微难一些。【参考方案15】:

使用在任何 servlet 容器中运行的任何 Web 应用程序(Tomcat、Jetty、GlassFish 等等……)。连续重新部署应用程序 10 或 20 次(只需触摸服务器自动部署目录中的 WAR 就足够了。

除非有人实际对此进行了测试,否则在几次重新部署后您很可能会收到 OutOfMemoryError,因为应用程序没有注意自行清理。通过此测试,您甚至可能会在您的服务器中发现一个错误。

问题是,容器的生命周期比应用程序的生命周期长。您必须确保容器可能对应用程序的对象或类的所有引用都可以被垃圾回收。

如果只有一个引用在您的 Web 应用程序的取消部署中幸存下来,则相应的类加载器以及因此您的 Web 应用程序的所有类都不能被垃圾回收。

您的应用程序启动的线程、ThreadLocal 变量、日志附加程序是导致类加载器泄漏的常见原因。

【讨论】:

这不是因为内存泄漏,而是因为类加载器没有卸载之前的一组类。因此不建议在不重启服务器(不是物理机,而是应用服务器)的情况下重新部署应用服务器。我在 WebSphere 上看到过同样的问题。【参考方案16】:

关于 PermGen 和 XML 解析,我曾经有过一次很好的“内存泄漏”。 我们使用的 XML 解析器(我不记得是哪一个了)对标签名称执行了 String.intern(),以便更快地进行比较。 我们的一位客户提出了一个绝妙的想法,即不将数据值存储在 XML 属性或文本中,而是作为标记名存储,因此我们的文档如下:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

事实上,他们没有使用数字,而是使用更长的文本 ID(大约 20 个字符),这些 ID 是独一无二的,每天以 10-15 百万的速度出现。这使得每天产生 200 MB 的垃圾,这不再需要,也永远不会被 GC(因为它在 PermGen 中)。我们将 permgen 设置为 512 MB,因此内存不足异常 (OOME) 大约需要两天时间才能到达...

【讨论】:

只是挑剔您的示例代码:我认为数字(或以数字开头的字符串)不允许作为 XML 中的元素名称。 请注意,对于 JDK 7+,这不再适用,其中字符串实习发生在堆上。有关详细的文章,请参阅本文:java-performance.info/string-intern-in-java-6-7-8 那么,我认为使用 StringBuffer 代替 String 可以解决这个问题?不会吗?【参考方案17】:

什么是内存泄漏:

这是由 bug糟糕的设计引起的。 太浪费内存了。 随着时间的推移会变得更糟。 垃圾收集器无法清理它。

典型例子:

对象缓存是一个很好的起点。

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)

    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;

您的缓存会不断增长。很快整个数据库就被吸进了内存。更好的设计使用 LRUMap(仅在缓存中保留最近使用的对象)。

当然,你可以让事情变得更复杂:

使用 ThreadLocal 构造。 添加更多复杂的参考树。 或由第三方库引起的泄漏。

经常发生的事情:

如果此 Info 对象引用了其他对象,则该对象又引用了其他对象。在某种程度上,您也可以认为这是某种内存泄漏(由糟糕的设计引起)。

【讨论】:

稍微不相关的说明:有一句流行的说法:编程中只有两件事困难:命名和缓存失效。【参考方案18】:

面试官可能正在寻找类似下面的代码的循环引用(顺便说一下,它只会在使用引用计数的非常旧的 JVM 中泄漏内存,现在已经不是这种情况了)。但这是一个非常模糊的问题,因此这是展示您对 JVM 内存管理的理解的绝佳机会。

class A 
    B bRef;


class B 
    A aRef;


public class Main 
    public static void main(String args[]) 
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
     /* main */

那么你可以解释一下,使用引用计数,上面的代码会泄漏内存。但是大多数现代 JVM 不再使用引用计数。大多数使用清扫垃圾收集器,它实际上会收集这些内存。

接下来您可能会解释如何创建一个具有底层原生资源的对象,如下所示:

public class Main 
    public static void main(String args[]) 
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    

然后你可以解释这是技术上的内存泄漏,但实际上泄漏是由 JVM 中的本机代码分配底层本机资源引起的,这些资源没有被你的 Java 代码释放。

归根结底,对于现代 JVM,您需要编写一些 Java 代码来分配超出 JVM 感知范围的本机资源。

【讨论】:

【参考方案19】:

我认为没有人使用内部类示例很有趣。如果你有一个内部类;它固有地维护对包含类的引用。当然,从技术上讲,这不是内存泄漏,因为 Java 最终会清理它;但这可能会导致课程停留的时间比预期的要长。

public class Example1 
  public Example2 getNewExample2() 
    return this.new Example2();
  
  public class Example2 
    public Example2() 
  

现在,如果您调用 Example1 并得到一个 Example2 丢弃 Example1,那么您本质上仍然有一个指向 Example1 对象的链接。

public class Referencer 
  public static Example2 GetAnExample2() 
    Example1 ex = new Example1();
    return ex.getNewExample2();
  

  public static void main(String[] args) 
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  

我还听说过一个传言,如果你有一个变量存在的时间超过了特定的时间; Java 假定它永远存在,并且如果代码中无法再访问它,实际上将永远不会尝试清理它。但这是完全未经证实的。

【讨论】:

内部类很少成为问题。它们是一个简单的案例,很容易被发现。谣言也只是谣言。 “谣言”听起来像是有人对分代 GC 的工作原理读了一半。长期存在但现在无法访问的对象确实可以保留一段时间并占用空间,因为 JVM 将它们从年轻代中提升出来,因此它可以停止每次通过时检查它们。按照设计,他们会避开“清理我的 5000 条临时字符串”这一无聊的通行证。但它们不是不朽的。它们仍然有资格被收集,如果 VM 被 RAM 束缚,它最终将运行完整的 GC 扫描并收回该内存。【参考方案20】:

我最近遇到了一个由 log4j 引起的内存泄漏情况。

Log4j 有这种称为Nested Diagnostic Context(NDC) 的机制,它是一种用于区分来自不同来源的交错日志输出的工具。 NDC工作的粒度是线程,所以它会分别区分不同线程的日志输出。

为了存储线程特定的标签,log4j 的 NDC 类使用一个 Hashtable,它由 Thread 对象本身作为键(而不是线程 id),因此直到 NDC 标签留在内存中,所有挂起的对象线程对象也留在内存中。在我们的 Web 应用程序中,我们使用 NDC 使用请求 id 标记日志输出,以将日志与单个请求分开。将 NDC 标记与线程相关联的容器也在从请求返回响应时将其删除。问题发生在处理请求的过程中,产生了一个子线程,类似于以下代码:

pubclic class RequestProcessor 
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() 
           public void run() 
               logger.info("Child thread spawned")
               for(String s:hugeList) 
                   ....
               
           
        .start();
    
    

因此,NDC 上下文与生成的内联线程相关联。作为这个 NDC 上下文的关键的线程对象是内联线程,它有一个巨大的列表对象。因此,即使在线程完成它正在做的事情之后,对 hugeList 的引用仍然被 NDC 上下文 Hastable 保持活动状态,从而导致内存泄漏。

【讨论】:

这太糟糕了。您应该检查这个在记录到文件时分配零内存的日志库:mentalog.soliveirajr.com +1 您是否知道 slf4j/logback 中的 MDC 是否存在类似问题(同一作者的后续产品)?我即将对源进行深入研究,但想先检查一下。不管怎样,谢谢你发布这个。【参考方案21】:

创建一个静态 Map 并不断添加硬引用。这些永远不会被垃圾收集。

public class Leaker 
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value)  Leaker.CACHE.put(key, value); 

【讨论】:

这是怎么泄漏的?它正在做你要求它做的事情。如果这是泄漏,那么在任何地方创建和存储对象就是泄漏。 我同意@Falmarri。我没有看到那里有泄漏,你只是在创建对象。您当然可以使用另一种称为“removeFromCache”的方法“回收”刚刚分配的内存。泄漏是指您无法回收内存。 我的观点是,不断创建对象的人,可能会将它们放入缓存中,如果他们不小心,最终可能会出现 OOM 错误。 @duffymo:但这并不是问题真正要问的。这与简单地耗尽所有内存无关。 cmets 反对这个答案非常有趣,因为超过 2000 票的接受答案说的是完全相同的事情,尽管是以一种高度混淆的方式。 ThreadLocals 并不神奇——它们只是 Map 中的条目,由 Thread 中的成员变量指向。【参考方案22】:

每个人总是忘记原生代码路由。这是一个简单的泄漏公式:

    声明本机方法。 在本机方法中,调用malloc。不要打电话给free。 调用本机方法。

请记住,本机代码中的内存分配来自 JVM 堆。

【讨论】:

【参考方案23】:

您可以通过在该类的 finalize 方法中创建一个类的新实例来创建移动内存泄漏。如果终结器创建多个实例,则加分。这是一个简单的程序,它会根据您的堆大小在几秒到几分钟之间泄漏整个堆:

class Leakee 
    public void check() 
        if (depth > 2) 
            Leaker.done();
        
    
    private int depth;
    public Leakee(int d) 
        depth = d;
    
    protected void finalize() 
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    


public class Leaker 
    private static boolean makeMore = true;
    public static void done() 
        makeMore = false;
    
    public static void main(String[] args) throws InterruptedException 
        // make a bunch of them until the garbage collector gets active
        while (makeMore) 
            new Leakee(0).check();
        
        // sit back and watch the finalizers chew through memory
        while (true) 
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        
    

【讨论】:

【参考方案24】:

我想还没有人说过:你可以通过重写 finalize() 方法来复活一个对象,这样 finalize() 就会在某处存储 this 的引用。垃圾收集器只会在对象上调用一次,因此之后该对象将永远不会被销毁。

【讨论】:

这是不真实的。 finalize() 不会被调用,但是一旦没有更多的引用,对象就会被收集。垃圾收集器也没有被“调用”。 这个答案有误导性,finalize()方法只能被JVM调用一次,但这并不意味着如果对象复活然后再次取消引用它就不能被重新垃圾收集。如果finalize()方法中有资源关闭代码,那么这段代码将不会再次运行,这可能会导致内存泄漏。【参考方案25】:

我最近遇到了一种更微妙的资源泄漏。 我们通过类加载器的getResourceAsStream打开资源,碰巧输入流句柄没有关闭。

嗯,你可能会说,真是个白痴。

嗯,有趣的是:通过这种方式,您可以泄漏底层进程的堆内存,而不是从 JVM 的堆中泄漏。

您只需要一个 jar 文件,其中包含一个文件,该文件将从 Java 代码中引用。 jar 文件越大,分配的内存就越快。

您可以使用以下类轻松创建这样的 jar:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator 
    public static void main(String[] args) throws IOException 
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) 
            zos.write((int) (Math.round(Math.random()*100)+20));
        
        zos.closeEntry();
        zos.close();
    

只需粘贴到名为 BigJarCreator.java 的文件中,从命令行编译并运行它:

javac BigJarCreator.java
java -cp . BigJarCreator

等等:您在当前工作目录中找到一个 jar 存档,其中包含两个文件。

让我们创建第二个类:

public class MemLeak 
    public static void main(String[] args) throws InterruptedException 
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) 
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    


这个类基本上什么都不做,只是创建未引用的 InputStream 对象。这些对象将立即被垃圾收集,因此不会增加堆大小。 对于我们的示例来说,从 jar 文件加载现有资源很重要,而且大小在这里很重要!

如果您有疑问,请尝试编译并启动上面的类,但请确保选择合适的堆大小(2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

您不会在此处遇到 OOM 错误,因为没有保留任何引用,所以无论您在上面的示例中选择了多大的 ITERATIONS,应用程序都会继续运行。 除非应用程序进入等待命令,否则进程的内存消耗(在顶部 (RES/RSS) 或进程资源管理器中可见)会增长。在上面的设置中,它将分配大约 150 MB 的内存。

如果您希望应用程序安全运行,请在其创建位置关闭输入流:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

并且您的进程不会超过 35 MB,与迭代次数无关。

相当简单和令人惊讶。

【讨论】:

【参考方案26】:

另一种可能造成巨大内存泄漏的方法是保存对 Map.Entry&lt;K,V&gt; 的引用,而不是 TreeMap

很难评估为什么这仅适用于TreeMaps,但通过查看实现,原因可能是:TreeMap.Entry 存储对其兄弟的引用,因此如果 TreeMap 准备好已收集,但其他一些类持有对其任何 Map.Entry 的引用,则 整个 Map 将被保留到内存中。


真实场景:

想象一下,有一个 db 查询返回一个大的 TreeMap 数据结构。人们通常使用TreeMaps 作为保留元素插入顺序。

public static Map<String, Integer> pseudoQueryDatabase();

如果查询被多次调用,并且对于每个查询(因此,对于每个返回的 Map),您在某处保存一个 Entry,内存将不断增长。

考虑以下包装类:

class EntryHolder 
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) 
        this.entry = entry;
    

应用:

public class LeakTest 

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() 
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> 
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) 
                if (entry.getValue().equals(index)) 
                    holdersCache.add(new EntryHolder(entry));
                    break;
                
            
            // to observe behavior in visualvm
            try 
                Thread.sleep(500);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        );

    

    public static Map<String, Integer> pseudoQueryDatabase() 
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    

    public static void main(String[] args) throws Exception 
        new LeakTest().run();
    

在每次pseudoQueryDatabase() 调用之后,map 实例应该准备好收集,但不会发生,因为至少有一个 Entry 存储在其他地方。

根据您的jvm 设置,应用程序可能会在早期由于OutOfMemoryError 而崩溃。

您可以从这张visualvm 图表中看到内存是如何不断增长的。

哈希数据结构 (HashMap) 不会发生同样的情况。

这是使用HashMap时的图表。

解决方案?直接保存键/值(您可能已经这样做了)而不是保存Map.Entry


我写了一个更广泛的基准here。

【讨论】:

【参考方案27】:

正如很多人所建议的那样,资源泄漏相当容易导致 - 就像 JDBC 示例一样。实际的 memory 泄漏要困难一些 - 特别是如果您不依赖 JVM 的残缺部分来为您做这件事...

创建占用空间非常大的对象然后无法访问它们的想法也不是真正的内存泄漏。如果没有东西可以访问它,那么它将被垃圾收集,如果有东西可以访问它,那么它就不是泄漏......

使用的一种方法——我不知道它是否仍然有效——是有一个三深的循环链。就像对象 A 引用了对象 B,对象 B 引用了对象 C,而对象 C 引用了对象 A。GC 足够聪明地知道两个深链 - 如 A B - 如果 A 和 B 不能被其他任何东西访问,可以安全地收集,但无法处理三向链......

【讨论】:

已经有一段时间没有这种情况了。现代 GC 知道如何处理循环引用。【参考方案28】:

面试官可能一直在寻找循环参考解决方案:

    public static void main(String[] args) 
        while (true) 
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        
    

这是引用计数垃圾收集器的经典问题。然后你会礼貌地解释说 JVM 使用了一种更复杂的算法,没有这个限制。

【讨论】:

这是引用计数垃圾收集器的经典问题。 即使在 15 年前,Java 也没有使用引用计数。参考。计数也比 GC 慢。 不是内存泄漏。只是一个无限循环。 @Esben 在每次迭代中,之前的first 没有用处,应该被垃圾回收。在引用计数垃圾收集器中,对象不会被释放,因为它(本身)有一个活动引用。无限循环是为了证明泄漏:当你运行程序时,内存会无限增加。 @rds @ Wesley Tarle 假设循环不是无限的。还会有内存泄漏吗?【参考方案29】:

线程在终止之前不会被收集。它们充当垃圾收集的roots。它们是少数不会仅仅通过忘记它们或清除对它们的引用而不会被回收的对象之一。

考虑:终止工作线程的基本模式是设置线程看到的一些条件变量。线程可以定期检查变量并将其用作终止信号。如果变量没有声明volatile,那么线程可能看不到对变量的更改,因此它不会知道终止。或者想象一下,如果某些线程想要更​​新共享对象,但在尝试锁定它时出现死锁。

如果您只有少数线程,这些错误可能会很明显,因为您的程序将停止正常工作。如果您有一个线程池可以根据需要创建更多线程,那么过时/卡住的线程可能不会被注意到,并且会无限期地累积,从而导致内存泄漏。线程可能会在您的应用程序中使用其他数据,因此也会阻止收集它们直接引用的任何数据。

作为一个玩具示例:

static void leakMe(final Object object) 
    new Thread() 
        public void run() 
            Object o = object;
            for (;;) 
                try 
                    sleep(Long.MAX_VALUE);
                 catch (InterruptedException e) 
            
        
    .start();

随便调用System.gc(),但是传递给leakMe的对象永远不会死。

【讨论】:

@Spidey 没有什么是“卡住”的。调用方法迅速返回,传递的对象永远不会被回收。这正是泄漏。 在程序的整个生命周期中,您将有一个线程“运行”(或休眠,等等)。这对我来说不算泄漏。即使您没有完全使用它,池也不算是泄漏。 @Spidey “在程序的整个生命周期中,你都会拥有一个 [东西]。这对我来说不算泄漏。”你听到自己的声音了吗? @Spidey 如果您将进程知道的内存计算为没有泄漏,那么这里的所有答案都是错误的,因为进程总是跟踪其虚拟地址空间中的哪些页面被映射。当进程终止时,操作系统通过将页面放回空闲页面堆栈来清除所有泄漏。将其带到下一个极端,可以通过指出 RAM 芯片或磁盘交换空间中的任何物理位都没有物理错位或损坏来击败任何有争议的泄漏,因此您可以关闭计算机并再次打开以清理任何泄漏。 泄漏的实际定义是内存丢失,我们不知道,因此无法执行必要的程序来回收它;我们将不得不拆除并重建整个内存空间。像这样的流氓线程可能会通过死锁或狡猾的线程池实现自然而然地出现。这些线程引用的对象,即使是间接引用,现在也不会被收集,因此我们拥有在程序生命周期内不会自然回收或重用的内存。我认为这是一个问题。尤其是内存泄漏。【参考方案30】:

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

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

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

【讨论】:

以上是关于如何在 Java 中创建内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Java 中创建内存泄漏?

如何释放在子对话框中创建的 CWin 对象以避免内存泄漏

为啥在 swift 中创建字符串时会出现内存泄漏?

如何防止 CompileAssemblyFromSource 泄漏内存?

如何找到内存泄漏?

如何使用模块化代码片段中的LeakCanary检测内存泄漏?