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

3720

我最近参加了一次面试,其中被要求使用Java创建一个内存泄漏

不用说,我感到很笨,因为我不知道如何开始创建。

你可以给一个例子吗?


48
具有讽刺意味的是,每个非平凡的Java程序所面临的更难的问题是如何创建内存泄漏! - Peter - Reinstate Monica
3
不断往容器中添加新对象,却忘记添加删除它们的代码,或者实现部分工作的代码无法在程序运行时清理所有对象。 - Galik
5
Java服务器系统中最常见的内存泄漏是在共享状态下发生的,例如请求之间共享的缓存和服务。许多答案似乎过于复杂,忽略了这个明显而常见的领域。可能存在一种常见的泄漏模式,即具有请求作用域键(例如某种自定义缓存)的应用程序范围Map。 - Thomas W
61个回答

48

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

"内存泄漏在计算机科学中(或在此上下文中泄漏)是指当计算机程序消耗内存但无法将其释放回操作系统时发生的情况。"(维基百科)

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

到目前为止,所有其他答案在我看来都不是真正的内存泄漏。它们都旨在通过快速填充内存来占用有意义的空间。但是,您随时可以取消引用所创建的对象,从而释放内存,因此不存在泄漏。 acconrad的答案实际上很接近,因为他的解决方案就是通过将垃圾收集器强制置于无限循环中来“崩溃”垃圾收集器。

长话短说:使用JNI编写Java库时,可能会出现手动内存管理,导致内存泄漏。如果调用这个库,你的Java进程将会泄露内存。或者JVM存在缺陷,导致JVM失去内存。JVM可能存在一些错误,甚至可能已知一些错误,因为垃圾回收并不是那么简单,但这仍然是一个错误。从设计上讲,这是不可能发生的。你可能会询问一些受此类错误影响的Java代码。很抱歉我不知道有没有这样的代码,而且在下一个Java版本中也可能已经修复了这个错误。


17
这是一个非常狭隘(且不太有用)的内存泄漏定义。从实际角度出发,唯一有意义的定义是“内存泄漏是指程序在数据不再被需要后仍继续保持已分配内存的任何情况”。 - Mason Wheeler
“维基百科”上该术语的定义已经改变。现在它是这样的:“在计算机科学中,内存泄漏是一种资源泄漏类型,当计算机程序以不正确的方式管理内存分配时,不再需要的内存不会被释放。” 这是几乎所有海报使用的定义,但不包括此海报。 - Lii

44

这里有一个简单同时也可怕的例子,来自于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实例被丢弃后仍然保留原始长字符串和派生的子字符串在内存中。


21
substring() 方法在 Java 7 中创建一个新的字符串(这是一种新的行为)。 - anstarovoyt

41

在GUI代码中,一个常见的例子是创建小部件/组件并将监听器添加到一些静态/应用程序作用域对象中,然后在小部件被销毁时不删除该监听器。这不仅导致内存泄漏,而且当您正在侦听的任何内容触发事件时,也会产生性能影响,因为所有旧的监听器都会被调用。


41

将任何在任何servlet容器中运行的Web应用程序(TomcatJettyGlassFish等)重新部署10或20次(只需触摸服务器的自动部署目录中的WAR即可),如果没有进行测试,很有可能在几次重新部署后会出现OutOfMemoryError,因为该应用程序没有注意清理自身。您甚至可能会在此测试中发现服务器中的错误。

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

如果在Web应用程序取消部署后仍然存在一个引用,则相应的类加载器以及由此导致的Web应用程序的所有类都无法进行垃圾回收。

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


2
这不是由于内存泄漏,而是因为类加载器没有卸载先前的类集。因此,不建议在不重启服务器(而不是物理机器,而是应用服务器)的情况下重新部署应用服务器。我曾经在WebSphere中看到过同样的问题。 - Sven
@Sven:这就是内存泄漏。在这种情况下,类加载器没有卸载先前的类集,导致了内存泄漏。 - Lii

38

也许可以通过JNI使用外部本地代码?

纯Java几乎不可能。

但这是关于“标准”类型的内存泄漏,当您无法再访问内存,但它仍被应用程序所拥有。相反,您可以保留对未使用的对象的引用或打开流而不关闭它们。


28
这取决于“内存泄漏”的定义。如果是指“仍被占用但不再需要的内存”,那么在Java中很容易实现。如果是指“已分配但代码根本无法访问的内存”,那么就稍微困难些了。 - Joachim Sauer

36

我曾经在PermGen和XML解析方面遇到过一个很好的"内存泄漏"问题。 我们使用的XML解析器(我不记得是哪个了)对标签名称进行了String.intern()操作,以加快比较速度。 我们的一个客户有一个绝妙的想法,即将数据值存储在标签名中,而不是XML属性或文本中,因此我们有了这样的一个文档:

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

事实上,他们没有使用数字,而是使用较长的文本ID(约20个字符),这些ID是唯一的,每天达到1000-1500万个。这意味着每天产生大约200 MB的垃圾数据,这些数据从未被再次使用,并且永远不会被GC回收(因为它们位于PermGen中)。我们将Permgen设置为512 MB,因此需要大约两天才能出现内存溢出异常(OOME)...


4
仅仅是挑剔一下你的示例代码:我认为在XML中不允许使用数字(或以数字开头的字符串)作为元素名称。 - Paŭlo Ebermann
请注意,对于JDK 7+,字符串内部化发生在堆上,这与以前的情况不同。有关详细信息,请参阅此文章:http://java-performance.info/string-intern-in-java-6-7-8/。 - jmiserez
那么,我认为使用StringBuffer代替String会解决这个问题?不是吗? - anubhs

29

面试官可能正在寻找类似下面的循环引用代码(顺带一提,这种代码只会在使用引用计数的非常旧的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正常范围感知的本地资源。


1
没有任何JVM使用引用计数。您可能将早期的Microsoft JavaScript实现与Java混淆了。 - Erwin Bolwidt

27

什么是内存泄漏:

  • 它由 错误糟糕的设计 导致。
  • 它是一种浪费内存的情况。
  • 随着时间的推移,情况会变得更加严重。
  • 垃圾回收器无法清理它。

典型案例:

对象缓存是搞砸事情的好起点。

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构造。
  • 添加更多复杂的引用树
  • 或由第三方库引起的泄漏。

经常发生的事情:

如果此信息对象具有对其他对象的引用,而这些对象又具有对其他对象的引用。从某种意义上说,您也可以将其视为某种内存泄漏(由于糟糕的设计引起)。


稍微离题一下:有一句流行的话说:“编程中只有两件难事:命名和缓存失效。” - bvdb

23

我觉得有趣的是没有人使用内部类的例子。如果你有一个内部类,它会自然地保留对包含类的引用。当然,这并不是严格意义上的内存泄漏,因为Java最终会清理它;但这可能导致类的存在时间比预期要长。

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

如果您调用 Example1 并获得一个 Discarding Example1 的 Example2,则您将本质上仍然具有到 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会认为它永远存在,并且如果在代码中无法访问它,Java实际上不会尝试清除它。但这完全没有经过验证。


2
内部类很少成为问题。它们是一个简单的情况,非常容易检测。这个谣言只是谣言。 - bestsss
2
这个“谣言”听起来像是有人半读了一下关于分代GC如何工作的内容。长寿但现在无法访问的对象确实可能会在一段时间内占用空间,因为JVM将它们从年轻代提升出去,以便停止每次检查它们。它们将通过设计逃避微不足道的“清理我的5000个临时字符串”操作。但它们并不是不朽的。它们仍然有资格进行收集,如果VM缺乏RAM,它最终会运行完整的GC扫描并重新获取该内存。 - cHao

23

我最近遇到了一个内存泄漏的情况,是由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上下文哈希表仍然保留对hugeList的引用,从而导致内存泄漏。


太糟糕了。你应该检查一下这个日志记录库,它在日志记录到文件时不会分配任何内存:http://mentalog.soliveirajr.com - TraderJoeChicago
+1 你知道MDC在slf4j/logback(同一作者的后继产品)中是否存在类似问题吗?我即将深入研究源代码,但想先确认一下。无论如何,感谢您发布这篇文章。 - sparc_spread

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接