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

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

什么样的例子?


当前回答

就像这样!

public static void main(String[] args) {
    List<Object> objects = new ArrayList<>();
    while(true) {
        objects.add(new Object());
    }
}

其他回答


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

public class Main {
    public static void main(String args[]) {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            ((Unsafe) f.get(null)).allocateMemory(2000000000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Java中的内存泄漏不是典型的C/C++内存泄漏。

要了解JVM的工作原理,请阅读了解内存管理。

基本上,重要的部分是:

标记和扫描模型JRockit JVM使用标记和清除垃圾收集模型执行整个堆的垃圾收集。标记和扫描垃圾收集包括两个阶段,标记阶段和扫描阶段。在标记阶段,可以从Java访问的所有对象线程、本机句柄和其他根源标记为活动的,如以及可从这些对象访问的对象,等等向前地此过程识别并标记所有静止的对象使用,其余的可以被视为垃圾。在扫描阶段,将遍历堆以查找活动对象。这些差距记录在免费列表中可用于新对象分配。JRockit JVM使用标记和扫描的两个改进版本模型一种是同时进行标记和扫描,另一种是平行标记和扫描。你也可以将这两种策略结合起来例如主要是并发标记和并行扫描。

因此,在Java中创建内存泄漏;最简单的方法是创建一个数据库连接,做一些工作,而不是Close();然后在保持范围内的同时生成新的数据库连接。例如,这在循环中并不难做到。如果您有一个工作人员从队列中拉出并推送到数据库,那么您可以通过忘记Close()连接或在不需要时打开连接等方式轻松创建内存泄漏。

最终,您将通过忘记Close()连接来消耗已分配给JVM的堆。这将导致JVM垃圾疯狂收集;最终导致java.lang.OutOfMemoryError:java堆空间错误。应该注意,该错误可能并不意味着存在内存泄漏;这可能意味着你没有足够的记忆;例如,Cassandra和Elasticsearch等数据库可能会抛出错误,因为它们没有足够的堆空间。

值得注意的是,所有GC语言都是如此。以下是我作为SRE工作的一些例子:

Node.js使用Redis作为队列;开发团队每12小时创建一次新连接,但忘记关闭旧连接。最终,节点是OOMd,因为它消耗了所有内存。去吧(我犯了这个罪);使用JSON.Unmarshal解析大型JSON文件,然后通过引用传递结果并保持其打开状态。最终,这导致整个堆被我打开以解码JSON的意外引用所消耗。

如果您不了解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永远不会收集单例实例。当部署应用程序的新实例时,通常会创建一个新的类加载器,而由于单例,前一个类加载器将继续存在。

您可以尝试使用while循环使许多缓冲的读取器尝试同时打开同一个文件,条件决不为false。最上面的樱桃是这些永远不会关闭。

我想就如何使用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将分析内存并向您提供有关内存泄漏的可疑信息。