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

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

什么样的例子?


当前回答

GUI代码中的一个常见示例是创建小部件/组件并向某个静态/应用程序范围的对象添加侦听器,然后在小部件被破坏时不删除侦听器。不仅会出现内存泄漏,而且性能也会受到影响,因为无论你听什么都会引发事件,所有的老听众都会被调用。

其他回答

要做的一件简单的事情是使用带有不正确(或不存在)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.

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

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

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

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

Log4j有一种称为嵌套诊断上下文(NDC)的机制,它是一种区分不同来源的交织日志输出的工具。NDC工作的粒度是线程,因此它区分不同线程的日志输出。

为了存储线程特定的标记,log4j的NDC类使用一个Hashtable,该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对象。因此,即使线程完成了它正在做的事情,对hugeList的引用也会被NDC上下文Hastable保持活动状态,从而导致内存泄漏。

我最近遇到了一种更微妙的资源泄漏。我们通过类加载器的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,与迭代计数无关。

非常简单和令人惊讶。

这很简单:

Object[] o = new Object[]{};
while(true) {
    o = new Object[]{o};
}