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

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

什么样的例子?


当前回答

以在任何servlet容器(Tomcat、Jetty、GlassFish等)中运行的任何web应用程序为例。连续重新部署应用程序10或20次(只需在服务器的autodeploy目录中触摸WAR即可)。

除非有人真的测试过,否则在重新部署几次之后,很可能会出现OutOfMemoryError,因为应用程序没有注意自己的清理。通过此测试,您甚至可以在服务器中发现错误。

问题是,容器的生存期比应用程序的生存期长。您必须确保容器对应用程序的对象或类的所有引用都可以被垃圾收集。

如果只有一个引用在web应用程序的取消部署后仍然存在,则相应的类加载器以及web应用程序中的所有类都不能被垃圾收集。

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

其他回答

这里的大多数例子都“过于复杂”。它们是边缘案例。在这些例子中,程序员犯了一个错误(比如不要重新定义equals/hashcode),或者被JVM/JAVA的一个极端情况(用静态加载类…)所咬。我认为这不是面试官想要的例子,甚至不是最常见的例子。

但内存泄漏的情况确实更简单。垃圾收集器只释放不再引用的内容。我们作为Java开发人员并不关心内存。我们在需要时分配它,并让它自动释放。好的

但任何长寿命的应用程序都倾向于共享状态。它可以是任何东西,静态的,单态的。。。通常,非平凡的应用程序倾向于生成复杂的对象图。只是忘记将引用设置为null,或者更经常地忘记从集合中删除一个对象,就足以造成内存泄漏。

当然,如果处理不当,所有类型的侦听器(如UI侦听器)、缓存或任何长期共享状态都会产生内存泄漏。应该理解的是,这不是Java角落的情况,也不是垃圾收集器的问题。这是一个设计问题。我们设计为向长寿命对象添加侦听器,但在不再需要时不删除侦听器。我们缓存对象,但我们没有从缓存中删除它们的策略。

我们可能有一个复杂的图来存储计算所需的先前状态。但前一状态本身与前一状态相关联,依此类推。

就像我们必须关闭SQL连接或文件一样。我们需要设置对null的正确引用,并从集合中删除元素。我们应该有适当的缓存策略(最大内存大小、元素数量或计时器)。所有允许通知侦听器的对象必须同时提供addListener和removeListener方法。当这些通知器不再使用时,它们必须清除侦听器列表。

内存泄漏确实是可能的,而且完全可以预测。无需特殊的语言功能或角盒。内存泄漏要么是某些东西可能丢失的指示,甚至是设计问题。

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

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

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

这里有一个在纯Java中创建真正的内存泄漏(运行代码无法访问但仍存储在内存中的对象)的好方法:

应用程序创建一个长时间运行的线程(或者使用线程池更快地泄漏)。线程通过(可选的自定义)ClassLoader加载类。该类分配一大块内存(例如新字节[10000000]),在静态字段中存储对它的强引用,然后在ThreadLocal中存储对自身的引用。分配额外的内存是可选的(泄漏类实例就足够了),但这会使泄漏工作得更快。应用程序清除对自定义类或从中加载该类的ClassLoader的所有引用。重复

由于ThreadLocal在Oracle的JDK中的实现方式,这会造成内存泄漏:

每个线程都有一个私有字段threadLocals,它实际上存储线程本地值。此映射中的每个键都是对ThreadLocal对象的弱引用,因此在ThreadLocal对象被垃圾收集后,其条目将从映射中删除。但每个值都是一个强引用,因此当一个值(直接或间接)指向作为其键的ThreadLocal对象时,只要线程存在,该对象既不会被垃圾收集,也不会从映射中删除。

在本例中,强引用链如下所示:

线程对象→ threadLocals映射→ 示例类的实例→ 示例类→ 静态ThreadLocal字段→ ThreadLocal对象。

(ClassLoader在创建泄漏时并没有真正发挥作用,它只是因为这个额外的引用链而使泄漏变得更糟:example类→ 类加载器→ 它加载的所有类。在许多JVM实现中,尤其是在Java7之前,情况更糟,因为类和ClassLoader被直接分配到permagen中,根本不会被垃圾收集。)

这种模式的一个变体是,如果您经常重新部署碰巧使用ThreadLocal的应用程序,而这些应用程序在某种程度上指向自己,那么应用程序容器(如Tomcat)会像筛子一样泄漏内存。这种情况可能有许多微妙的原因,并且通常很难调试和/或修复。

更新:由于很多人一直在要求它,这里有一些示例代码显示了这种行为。

如果最大堆大小为X.Y1….Yn实例数

因此,总内存=每个实例的实例数X字节。如果X1……Xn是每个实例的字节数,则总内存(M)=Y1*X1++Yn*Xn。因此,如果M>X,它将超过堆空间。

以下可能是代码中的问题

使用更多实例变量,然后使用局部变量。每次都创建实例,而不是共享对象。未按需创建对象。操作完成后使对象引用为空。再次,在程序中需要时重新创建。

内存泄漏的情况有很多种。我遇到了一个,它暴露了一个不应该在其他地方暴露和使用的地图。

public class ServiceFactory {

    private Map<String, Service> services;

    private static ServiceFactory singleton;

    private ServiceFactory() {
        services = new HashMap<String, Service>();
    }

    public static synchronized ServiceFactory getDefault() {

        if (singleton == null) {
            singleton = new ServiceFactory();
        }
        return singleton;
    }

    public void addService(String name, Service serv) {
        services.put(name, serv);
    }

    public void removeService(String name) {
        services.remove(name);
    }

    public Service getService(String name, Service serv) {
        return services.get(name);
    }

    // The problematic API, which exposes the map.
    // and user can do quite a lot of thing from this API.
    // for example, create service reference and forget to dispose or set it null
    // in all this is a dangerous API, and should not expose
    public Map<String, Service> getAllServices() {
        return services;
    }

}

// Resource class is a heavy class
class Service {

}