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

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

什么样的例子?


当前回答

另一种可能造成巨大内存泄漏的方法是保存对TreeMap的Map.Entry<K,V>的引用。

很难理解为什么这只适用于TreeMaps,但通过查看实现,原因可能是:TreeMap.Entry存储了对其同级的引用,因此,如果TreeMaps准备好被收集,但其他类保存了对其Map.Intry的引用,则整个Map将保留在内存中。


现实生活场景:

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

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

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

考虑以下包装类:

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()调用之后,映射实例应该准备好进行收集,但这不会发生,因为至少有一个Entry存储在其他地方。

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

您可以从这个可视化虚拟机图中看到内存是如何保持增长的。

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

这是使用HashMap时的图形。

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


我在这里写了一个更广泛的基准。

其他回答

理论上你不能。Java内存模型阻止了这一点。但是,因为必须实现Java,所以可以使用一些警告。这取决于您可以使用什么:

如果可以使用本机,则可以分配以后不会放弃的内存。如果这是不可用的,那么Java有一个不为人知的小秘密。您可以请求一个不由GC管理的直接访问数组,因此可以很容易地用于造成内存泄漏。这由DirectByteBuffer提供(http://download.oracle.com/javase/1.5.0/docs/api/java/nio/ByteBuffer.html#allocateDirect(int))。如果不能使用其中任何一个,仍然可以通过欺骗GC来造成内存泄漏。JVM是使用一代垃圾收集来实现的。这意味着垃圾堆被划分为三个区域:年轻人、成年人和老年人。对象创建时从年轻区域开始。随着它被越来越多地使用,它逐渐发展到成人到老年人。最有可能到达接骨木区域的对象不会被垃圾收集。您无法确定对象是否泄漏,如果您请求停止并清理GC,它可能会清理它,但在很长一段时间内,它会泄漏。更多信息请访问(http://java.sun.com/docs/hotspot/gc1.4.2/faq.html)此外,类对象不需要是GC’ed。也许有办法做到这一点。

这里有一个非常简单的Java程序,它将耗尽空间

public class OutOfMemory {

    public static void main(String[] arg) {

        List<Long> mem = new LinkedList<Long>();
        while (true) {
            mem.add(new Long(Long.MAX_VALUE));
        }
    }
}

一些建议:

在servlet容器中使用commons日志记录(可能有点挑衅)在servlet容器中启动线程,不要从其运行方法返回在servlet容器中加载动画GIF图像(这将启动一个动画线程)

通过重新部署应用程序,可以“改善”上述效果;)

我最近偶然发现:

调用“newjava.util.zip。充气器();”而不调用“充气器.end()”

阅读http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5072161并将问题联系起来进行深入讨论。

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

因此,Connection的最终方法并不能清除所有内容。人们可能会发现,到数据库服务器的物理连接将持续几个垃圾收集周期,直到数据库服务器最终发现该连接不活动(如果存在),应该关闭。

即使JDBC驱动程序实现了finalize,编译器也可以在finalize期间抛出异常。由此产生的行为是,与现在“休眠”对象关联的任何内存都不会被编译器回收,因为finalize保证只被调用一次。

上述在对象完成过程中遇到异常的场景与另一种可能导致内存泄漏的场景有关——对象复活。对象复活通常是通过创建一个从另一个对象最终确定的对象的强引用来实现的。当对象复活被误用时,它将与其他内存泄漏源一起导致内存泄漏。

还有很多例子你可以想象出来

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

我在javax.swing.JPopupMenu中遇到了非常真实的内存泄漏。

我有一个GUI应用程序,它显示多个选项卡式文档。关闭文档后,如果在选项卡上的任何组件上使用了右键单击上下文菜单,它就会在内存中停留。这些菜单在选项卡之间共享,结果是,在调用popupMenu.show(component invoker,int x,int y)后,组件会作为菜单的“调用程序”静静地存在,直到下一次更改或被setInvoker(null)清除。间接地,调用者引用持久化了整个文档以及与之相关的所有内容。

值得注意的是,菜单只能以这种方式保存对旧组件的一个引用,因此这种内存泄漏不会在没有绑定的情况下增长。