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

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

什么样的例子?


当前回答

我最近修复的一个示例是创建新的GC和Image对象,但忘记调用dispose()方法。

GC javadoc代码段:

应用程序代码必须显式调用GC.dispose()方法以在以下情况下释放每个实例管理的操作系统资源不再需要这些实例。这一点尤为重要在Windows95和Windows98上可用的设备上下文数。

图像javadoc片段:

应用程序代码必须显式调用Image.dispose()方法在以下情况下释放每个实例管理的操作系统资源不再需要这些实例。

其他回答

我最近遇到了一种更微妙的资源泄漏。我们通过类加载器的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文件加载现有资源很重要,这里的大小很重要!

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

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

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

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

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

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

非常简单和令人惊讶。

我想就如何使用JVM中可用的工具监视应用程序的内存泄漏提供建议。它没有显示如何生成内存泄漏,但解释了如何使用最少的可用工具检测内存泄漏。

您需要首先监视Java内存消耗。

最简单的方法是使用JVM附带的jstat实用程序:

jstat -gcutil <process_id> <timeout>

它将报告每一代(年轻、老年和老年)的内存消耗和垃圾收集时间(年轻和完整)。

一旦您发现一个完整的垃圾收集执行得太频繁并且花费了太多时间,您就可以假设应用程序正在泄漏内存。

然后需要使用jmap实用程序创建内存转储:

jmap -dump:live,format=b,file=heap.bin <process_id>

然后需要使用内存分析器(例如Eclipse memory Analyzer(MAT))分析heap.bin文件。

MAT将分析内存并向您提供有关内存泄漏的可疑信息。

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

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

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

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

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

我将集中讨论线程,以展示非托管线程的危险性,甚至不希望触及摆动。

Runtime.addShutdownHook,不删除。。。然后,即使使用removeShutdownHook,由于ThreadGroup类中关于未启动线程的错误,它也可能无法被收集,从而有效地泄漏了ThreadGroup。JGroup在GossipRouter中有漏洞。创建一个线程,但不是启动它,它属于与上面相同的类别。创建线程继承ContextClassLoader和AccessControlContext,加上ThreadGroup和任何InheritedThreadLocal,所有这些引用都是潜在的泄漏,以及类加载器加载的所有类和所有静态引用,以及ja-ja。这种效果在整个j.u.c.Executor框架中尤其明显,该框架具有超简单的ThreadFactory接口,但大多数开发人员对潜在的危险一无所知。此外,许多库确实会根据请求启动线程(太多行业流行的库)。ThreadLocal缓存;这些在很多情况下都是邪恶的。我相信每个人都看到过很多基于ThreadLocal的简单缓存,但坏消息是:如果线程在上下文ClassLoader的生命周期中继续运行超过预期,这是一个非常好的小泄漏。除非确实需要,否则不要使用ThreadLocal缓存。当ThreadGroup本身没有线程,但仍保留子ThreadGroups时,调用ThreadGroup.destroy()。一个严重的泄漏,将阻止ThreadGroup从其父级中删除,但所有子级都无法枚举。使用WeakHashMap和值(in)直接引用键。如果没有堆转储,这很难找到。这适用于可能将硬引用保留回受保护对象的所有扩展弱/软引用。将java.net.URL与HTTP(S)协议一起使用,并从(!)加载资源。这一个是特殊的,KeepAliveCache在系统ThreadGroup中创建了一个新线程,该线程泄漏了当前线程的上下文类加载器。当不存在活动线程时,线程会在第一个请求时创建,因此您可能会幸运,或者只是泄漏。泄漏在Java7中已经修复,创建线程的代码正确地删除了上下文类加载器。创建类似线程的情况很少(如ImageFetcher,也已修复)。使用充气器InputStream在构造函数(例如PNGImageDecoder)中传递新的java.util.zip充气器(),而不调用充气器的end()。好吧,如果你只传递一个新的构造函数,就没有机会。。。是的,如果将其作为构造函数参数手动传递,则对流调用close()不会关闭充气机。这不是真正的泄漏,因为它将由终结器释放。。。当它认为有必要时。直到那一刻,它会严重消耗本地内存,导致Linux oom_killer肆无忌惮地终止进程。主要的问题是,在Java中完成是非常不可靠的,G1使其更糟,直到7.0.2。故事的寓意:尽快释放本土资源;终结器太差了。与java.util.zip.Deflater的情况相同。这一情况更糟,因为Deflater在java中需要内存,即总是使用15位(最大值)和8个内存级别(最大值为9)来分配数百KB的本地内存。幸运的是,Deflater没有被广泛使用,据我所知,JDK没有任何误用。如果手动创建放气器或充气器,请始终调用end()。最后两种方法中最棒的一点是:您无法通过常规的分析工具找到它们。

(我可以根据要求再添加一些我遇到的时间浪费者。)

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

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个字符将一直保存在内存中,直到您丢失了对子字符串的引用。

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