如何在Java中创建内存泄漏?

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

不用说,我觉得自己很愚蠢,甚至不知道如何开始创建一个。

一个例子是什么?

728088 次浏览

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

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

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

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

创建一个静态Map并不断向其添加硬引用。这些永远不会被垃圾回收。

public class Leaker {private static final Map<String, Object> CACHE = new HashMap<String, Object>();
// Keep adding until failure.public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }}

每当你把引用放在你不再需要的对象周围时,你就会有内存泄漏。有关内存泄漏如何在Java中表现出来以及你可以做些什么的示例,请参阅处理Java程序中的内存泄漏

如果您不理解JDBC,以下是一个非常毫无意义的示例。或者至少JDBC希望开发人员在丢弃Connection,StatementResultSet实例或丢失对它们的引用之前关闭它们,而不是依赖于实现finalize方法。

void doWork() {try {Connection conn = ConnectionFactory.getConnection();PreparedStatement stmt = conn.preparedStatement("some query");// executes a valid queryResultSet 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的最终方法不会清除所有内容。人们可能会发现,数据库服务器的物理Connection将持续几个垃圾回收机制周期,直到数据库服务器最终发现Connection不是活动的(如果是活动的)并且应该关闭。

即使JDBC驱动程序实现了finalize,编译器也可以在完成过程中抛出异常。结果是编译器不会回收与现在“休眠”对象关联的任何内存,因为finalize保证只被调用一次。

上述在对象终结过程中遇到异常的情况与另一种可能导致内存泄漏的情况有关-对象复活。对象复活通常是通过从另一个对象创建对该对象的强引用来故意完成的。当对象复活被滥用时,它将导致内存泄漏以及其他内存泄漏源。

还有很多你能想到的例子比如

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

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

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

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

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

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

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

ClassLoader在造成泄漏中并没有真正发挥作用,它只是因为这个额外的引用链而使泄漏变得更糟:示例类→ClassLoader→它加载的所有类。在许多JVM实现中甚至更糟,特别是在Java7之前,因为类和ClassLoader直接分配到permgen中,根本没有垃圾回收。)

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

更新:因为很多人一直在要求它,这是一些示例代码,显示了这种行为的实际效果

可能是潜在内存泄漏的最简单示例之一,以及如何避免它,是ArrayList.remove(int)的实现:

public E remove(int index) {RangeCheck(index);
modCount++;E oldValue = (E) elementData[index];
int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index + 1, elementData, index,numMoved);elementData[--size] = null; // (!) Let gc do its work
return oldValue;}

如果你自己实现它,你会考虑清除不再使用的数组元素(elementData[--size] = null)吗?那个引用可能会让一个巨大的对象保持活力…

GUI代码中的一个常见示例是创建小部件/组件并将侦听器添加到某些静态/应用程序范围的对象,然后在小部件被销毁时不删除侦听器。您不仅会得到内存泄漏,而且还会受到性能影响,因为当您正在监听的内容触发事件时,所有旧侦听器也会被调用。

我曾经有过一次关于PermGen和XML解析的“内存泄漏”。我们使用的XML解析器(我不记得是哪一个了)在标记名称上执行了String.intern(),以便更快地进行比较。我们的一个客户有一个好主意,不是将数据值存储在XML属性或文本中,而是作为标记名存储,所以我们有一个文档,如:

<data><1>bla</1><2>foo</>...</data>

事实上,他们没有使用数字,而是使用更长的文本ID(大约20个字符),这些ID是唯一的,并且以每天1500万的速度出现。这使得每天产生200MB的垃圾,再也不会被需要,也从来没有GCed(因为它是在PermGen中的)。我们将permgen设置为512MB,所以内存不足异常(OOME)大约花了两天时间到达…

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

  • File.deleteOnExit()-总是泄漏字符串,如果字符串是子字符串,则泄漏更严重(底层char[]也被泄漏)-在Java7子字符串也复制了#1,所以后者不适用;@Daniel,不需要投票。

我将集中精力在线程上,以展示非托管线程的危险,甚至不希望碰摆动。

  • Runtime.addShutdownHook并且没有删除…,然后即使使用了demveShutdown Hook,由于ThreadGroup类中关于未启动线程的bug,它可能无法收集,有效地泄漏了ThreadGroup。JGroup在戈西路由器中有泄漏。

  • 创建,但不是开始,Thread进入与上述相同的类别。

  • 创建线程继承了ContextClassLoaderAccessControlContext,加上ThreadGroup和任何InheritedThreadLocal,所有这些引用都是潜在的泄漏,以及类加载器加载的整个类和所有静态引用,以及ja-ja。这种效果在整个j. u. c. Execator框架中尤为明显,该框架具有超级简单的ThreadFactory接口,但大多数开发人员对潜伏的危险一无所知。此外,许多库确实应要求启动线程(行业流行库太多了)。

  • ThreadLocal缓存;在许多情况下是邪恶的。我相信每个人都见过很多基于ThreadLocal的简单缓存,坏消息是:如果线程持续运行超过上下文ClassLoader的预期,这是一个纯粹的不错的小漏洞。除非真的需要,否则不要使用ThreadLocal缓存。

  • 当ThreadGroup本身没有线程,但它仍然保留子线程组时调用ThreadGroup.destroy()。一个糟糕的泄漏将阻止ThreadGroup从其父级中删除,但所有子级都变得不可枚举。

  • 使用WeakHashMap并且值(in)直接引用键。如果没有堆转储,这很难找到。这适用于所有可能将硬引用保留回受保护对象的扩展Weak/SoftReference

  • 使用java.net.URL和HTTP(S)协议并从(!)加载资源。这个很特别,KeepAliveCache在系统ThreadGroup中创建了一个新线程,该线程泄漏了当前线程的上下文类加载器。该线程是在第一次请求时创建的,当没有活线程存在时,所以你可能很幸运,或者只是泄漏。泄漏已经在Java7中修复了,创建线程的代码正确地删除了上下文类加载器。

  • 在构造函数中使用InflaterInputStream传递new java.util.zip.Inflater()(例如PNGImageDecoder)而不是调用膨胀器的end()。好吧,如果你只传递new的构造函数,就没有机会了……是的,在流中调用close()不会关闭膨胀器,如果它是作为构造函数参数手动传递的。这不是真正的泄漏,因为它会被终结器释放……当它认为有必要时。直到那一刻,它严重消耗本机内存,它会导致Linuxoom_killer杀死进程而不受惩罚。主要问题是Java中的终结非常不可靠,G1在7.0.2之前使它变得更糟。这个故事的寓意:尽快释放本地资源;终结器太差了。

  • java.util.zip.Deflater的情况也是如此。这个情况要糟糕得多,因为Deflater在Java中需要内存,即总是使用15位(最大)和8个内存级别(最大9个)分配数百KB的本机内存。幸运的是,Deflater没有被广泛使用,据我所知JDK没有误用。如果您手动创建DeflaterInflater,请始终调用end()。最后两个最好的部分:您无法通过可用的普通分析工具找到它们。

(我可以根据要求添加更多我遇到的浪费时间的东西。

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

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

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

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

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

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

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

内存泄漏确实是可能的,并且是完全可预测的。不需要特殊的语言特性或角落情况。内存泄漏要么是可能缺少某些东西的指示,要么甚至是设计问题的指示。

包含对象引用的静态字段[尤其是最终字段]

class MemorableClass {static final ArrayList list = new ArrayList(100);}

(未关闭)打开流(文件、网络等)

try {BufferedReader br = new BufferedReader(new FileReader(inputFile));......} catch (Exception e) {e.printStackTrace();}

未关闭的连接

try {Connection conn = ConnectionFactory.getConnection();......} catch (Exception e) {e.printStackTrace();}

JVM的垃圾收集器无法访问的区域,例如通过本机方法分配的内存。

在Web应用程序中,一些对象存储在应用程序范围内,直到应用程序被显式停止或删除。

getServletContext().setAttribute("SOME_MAP", map);

不正确或不适当的JVM选项,例如JDK上IBMnoclassgc选项,可以防止未使用的类垃圾回收机制

IBMJDK设置

答案完全取决于面试官认为他们在问什么。

在实践中有可能使Java泄漏吗?当然有,在其他答案中也有很多例子。

但是有很多元问题可能会被问到?

  • 理论上“完美”的Java实现是否容易泄露?
  • 候选人是否理解理论和现实之间的区别?
  • 候选人是否了解垃圾回收机制的工作原理?
  • 或者在理想情况下,垃圾回收机制应该如何工作?
  • 他们知道他们可以通过本地接口调用其他语言吗?
  • 他们知道在其他语言中泄漏内存吗?
  • 候选人是否知道内存管理是什么,以及Java幕后发生了什么?

我把你的元问题解读为“在这种面试情况下,我有什么可以用的答案吗”。因此,我将重点关注面试技巧,而不是Java。我相信你更有可能在面试中重复不知道问题答案的情况,而不是需要知道如何Java泄密。所以,希望这会有所帮助。

你在面试中可以培养的最重要的技能之一就是学会积极地倾听问题,并与面试官一起提取他们的意图。这不仅可以让你以他们想要的方式回答他们的问题,还可以表明你拥有一些至关重要的沟通技巧。当涉及到在许多同样有才华的开发人员中进行选择时,我会雇佣那个在他们每次回答之前都会倾听、思考和理解的人。词性:动词短语

我可以从这里复制我的答案:最容易导致内存泄漏的Java

内存泄漏,在计算机科学中(或泄漏,在这种情况下),发生在计算机程序消耗内存但无法将其释放回操作系统时。

简单的答案是:你不能。Java会自动内存管理,并释放你不需要的资源。你不能阻止这种情况发生。它将总是能够释放资源。在手动内存管理的程序中,这是不同的。你可以使用malloc()在C中获得一些内存。要释放内存,你需要malloc返回的指针并对其调用free()。但是如果你不再有指针(覆盖,或超过生命周期),那么不幸的是,你无法释放这个内存,因此你有内存泄漏。

到目前为止,所有其他答案在我的定义中都不是真正的内存泄漏。它们都旨在快速用无意义的东西填充内存。但是在任何时候,你仍然可以取消引用你创建的对象,从而释放内存-->没有泄漏acconrad的回答非常接近,尽管我不得不承认,因为他的解决方案实际上是通过强制垃圾收集器进入无休止的循环来“崩溃”垃圾收集器)。

很长的答案是:你可以通过使用JNI为Java编写一个库来获得内存泄漏,该库可以进行手动内存管理,因此存在内存泄漏。如果你调用此库,你的Java进程将泄漏内存。或者,你可以在JVM中存在错误,因此JVM丢失内存。JVM中可能存在错误,甚至可能存在一些已知的错误,因为垃圾回收机制并不是那么微不足道,但它仍然是一个bug。根据设计,这是不可能的。你可能要求一些Java代码受到这样的bug的影响。对不起,我不知道一个,它可能不是一个bug在接下来的Java版本。

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

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

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

我想还没有人这样说过:你可以通过覆盖finaliz()方法来复活一个对象,这样finaliz()就会在某个地方存储this的引用。垃圾收集器只会在对象上调用一次,所以之后对象永远不会被销毁。

以在任何servlet容器(TomcatJetty玻璃鱼,无论什么…)中运行的任何Web应用程序为例。连续重新部署应用程序10或20次(只需触摸服务器自动部署目录中的战争就足够了。

除非有人真正测试过这个,否则在几次重新部署后,你很有可能会得到OutOfMemoryError,因为应用程序没有注意自己清理。你甚至可以通过此测试在服务器中找到一个bug。

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

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

由您的应用程序启动的线程、ThreadLocal变量、日志记录附加程序是导致类加载器泄漏的一些常见嫌疑人。

面试官可能一直在寻找循环参考解决方案:

    public static void main(String[] args) {while (true) {Element first = new Element();first.next = new Element();first.next.next = first;}}

这是引用计数垃圾收集器的一个经典问题。然后您会礼貌地解释JVM使用更复杂的算法,没有这个限制。

我认为没有人使用内部类示例很有趣。如果你有一个内部类;它本质上维护了对包含类的引用。当然,从技术上讲,这不是内存泄漏,因为Java最终会清理它;但这可能导致类挂起的时间比预期的要长。

public class Example1 {public Example2 getNewExample2() {return this.new Example2();}public class Example2 {public Example2() {}}}

现在,如果您调用它并得到一个丢弃它的实例2,您将本质上仍然有一个指向实例1对象的链接。

public class Referencer {public static Example2 GetAnExample2() {Example1 ex = new Example1();return ex.getNewExample2();}
public static void main(String[] args) {Example2 ex = Referencer.GetAnExample2();// As long as ex is reachable; Example1 will always remain in memory.}}

我还听到一个谣言,如果你有一个变量存在的时间超过了特定的时间;Java假设它将永远存在,并且如果无法在代码中访问,实际上永远不会尝试清理它。但这是完全未经验证的。

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

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 exposepublic Map<String, Service> getAllServices() {return services;}
}
// Resource class is a heavy classclass Service {
}

您可以使用sun.misc.不安全类进行内存泄漏。事实上,此服务类用于不同的标准类(例如在java.nio类中)。您不能直接创建此类的实例,但您可以使用反射来获取实例

代码无法在Eclipse IDE中编译-使用命令javac编译它(在编译过程中,您会收到警告)

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

public class TestUnsafe {
public static void main(String[] args) throws Exception{Class unsafeClass = Class.forName("sun.misc.Unsafe");Field f = unsafeClass.getDeclaredField("theUnsafe");f.setAccessible(true);Unsafe unsafe = (Unsafe) f.get(null);System.out.print("4..3..2..1...");try{for(;;)unsafe.allocateMemory(1024*1024);} catch(Error e) {System.out.println("Boom :)");e.printStackTrace();}}}

一种可能是为ArrayList创建一个包装器,它只提供一种方法:向ArrayList添加东西的方法。使ArrayList本身私有。现在,在全局范围内构造其中一个包装器对象(作为类中的静态对象)并使用最终关键字(例如public static final ArrayListWrapper wrapperClass = new ArrayListWrapper())对其进行限定。所以现在引用不能被改变。也就是说,wrapperClass = null不会起作用,也不能用于释放内存。但是除了向它添加对象之外,也没有办法对wrapperClass做任何事情。因此,你添加到wrapperClass的任何对象都不可能回收。

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

GC javadoc代码片段:

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

图片javadoc片段:

应用程序代码必须显式调用Image.dispose()方法释放每个实例管理的操作系统资源现在已经不需要了

这是一个简单/险恶的通过http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29

public class StringLeaker{private final String muchSmallerString;
public StringLeaker(){// Imagine the whole Declaration of Independence hereString veryLongString = "We hold these truths to be self-evident...";
// The substring here maintains a reference to the internal char[]// representation of the original string.this.muchSmallerString = veryLongString.substring(0, 1);}}

因为子字符串引用了原始更长字符串的内部表示,所以原始字符串会保留在内存中。因此,只要您有一个StringLeaker在玩,您也会在内存中拥有整个原始字符串,即使您可能认为您只是保留了一个单字符字符串。

避免存储对原始字符串的不必要引用的方法是这样做:

...this.muchSmallerString = new String(veryLongString.substring(0, 1));...

对于添加的badness,您还可以.intern()子字符串:

...this.muchSmallerString = veryLongString.substring(0, 1).intern();...

这样做将保留原始长字符串和派生子字符串在内存中,即使StringLeaker实例已被丢弃。

正如很多人所建议的,资源泄漏很容易导致-就像JDBC的例子一样。实际的内存泄漏有点困难-特别是如果您不依赖JVM的破碎部分来为您做这件事…

创建占用空间非常大的对象然后无法访问它们的想法也不是真正的内存泄漏。如果没有东西可以访问它,那么它将被垃圾收集,如果有东西可以访问它,那么它就不是泄漏…

不过,使用的一种工作方式——我不知道它是否仍然如此——是拥有一个三深循环链。就像在对象A有一个对对象B的引用一样,对象B有一个对对象C的引用,对象C有一个对对象A的引用。GC足够聪明,知道如果A和B不能被其他任何东西访问,但无法处理三向链,那么两个深链——就像A<-->B——可以安全地收集…

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

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

为了存储特定于线程的标签,log4j的NDC类使用一个由Thread对象本身(而不是线程id)键控的Hashtable,因此直到NDC标签留在内存中,挂在线程对象上的所有对象也留在内存中。在我们的网络应用程序中,我们使用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保持活动,从而导致内存泄漏。

每个人总是忘记本机代码路由。这是一个简单的泄漏公式:

  1. 声明一个本机方法。
  2. 在本机方法中,调用malloc。不要调用free
  3. 调用本机方法。

请记住,本机代码中的内存分配来自JVM堆。

理论上不能。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。可能有一种方法可以做到这一点。

您可以通过在类的finaliz方法中创建类的新实例来创建移动内存泄漏。如果终结器创建了多个实例,则加分。这是一个简单的程序,它会在几秒钟到几分钟之间泄漏整个堆,具体取决于您的堆大小:

class Leakee {public void check() {if (depth > 2) {Leaker.done();}}private int depth;public Leakee(int d) {depth = d;}protected void finalize() {new Leakee(depth + 1).check();new Leakee(depth + 1).check();}}
public class Leaker {private static boolean makeMore = true;public static void done() {makeMore = false;}public static void main(String[] args) throws InterruptedException {// make a bunch of them until the garbage collector gets activewhile (makeMore) {new Leakee(0).check();}// sit back and watch the finalizers chew through memorywhile (true) {Thread.sleep(1000);System.out.println("memory=" +Runtime.getRuntime().freeMemory() + " / " +Runtime.getRuntime().totalMemory());}}}

我在Java中看到的大多数内存泄漏都与进程不同步有关。

进程A通过tcp与B对话,并告诉进程B创建一些东西。B向资源发出一个ID,例如432423,A将其存储在对象中并在与B对话时使用。在某些时候,A中的对象被垃圾回收机制回收(可能是由于bug),但A从未告诉B(可能是另一个bug)。

现在A不再拥有它在B的RAM中创建的对象的ID,并且B不知道A不再引用该对象。实际上,对象被泄露了。

几点建议:

  • 在servlet容器中使用共享日志(可能有点挑衅)
  • 在servlet容器中启动线程并且不从其run方法返回
  • 在servlet容器中加载动画GIF图像(这将启动一个动画线程)

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

我最近偶然发现了这个:

  • 调用“newjava.util.zip.Inflater();”而不调用“Inflater.end()”

阅读http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5072161和链接的问题进行深入讨论。

Java“内存泄漏”主要是你使用了太多的内存,这与C中不同,C中你不再使用内存但忘记返回(释放)它。当面试官询问Java内存泄漏时,他们询问的是JVM内存使用率似乎一直在上升,他们认为定期重新启动JVM是最好的解决方案(除非面试官技术精通)。

所以回答这个问题,就像他们问是什么让JVM内存使用量随着时间的推移而增长一样。好的答案是在超时时间过长的Http会话中存储太多数据,或者实现不佳的内存缓存(单例),永远不会刷新旧条目。另一个潜在的答案是拥有大量JSP或动态生成的类。类被加载到一个名为PermGen的内存区域中,该区域通常很小,大多数JVM不实现类卸载。

我最近遇到了一种更微妙的资源泄漏。我们通过类加载器的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.javajava -cp . BigJarCreator

Et voilà:您在当前工作目录中找到一个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文件加载现有资源很重要,这里的大小很重要!

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

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

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

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

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

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

相当简单和令人惊讶。

Swing很容易使用对话框。创建一个JDialog,显示它,用户关闭它,然后泄漏!

您必须调用dispose()或配置setDefaultCloseOperation(DISPOSE_ON_CLOSE)

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

所以,总内存=每个实例的实例数X字节。如果X1…… Xn是每个实例的字节数,那么总内存(M)=Y1*X1+……+Yn*Xn。所以,如果M>X,它就超过了堆空间。

以下可能是代码中的问题

  1. 使用更多的实例变量,然后使用本地变量。
  2. 每次都创建实例,而不是池化对象。
  3. 不按需创建对象。
  4. 操作完成后使对象引用为空。再次,在程序中需要时重新创建。

这是一个非常简单的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));}}}

如果不使用压缩垃圾收集器,则可能会由于堆碎片而导致某种内存泄漏。

丢失的监听器是内存泄漏的一个很好的例子:对象被添加为监听器。当不再需要对象时,对该对象的所有引用都为空。然而,忘记从监听器列表中删除对象会使对象保持活动状态,甚至对事件做出响应,从而浪费内存和CPU。参见http://www.drdobbs.com/jvm/java-qa/184404011

从finaliz方法抛出一个未处理的异常。

线程直到终止才会被收集。它们是垃圾回收机制的。它们是少数几个不能通过忘记它们或清除对它们的引用来回收的对象之一。

考虑:终止工作线程的基本模式是设置线程看到的一些条件变量。线程可以定期检查变量并将其用作终止的信号。如果变量未声明volatile,那么线程可能看不到对变量的更改,因此它不知道终止。或者想象一下,如果一些线程想要更新一个共享对象,但在试图锁定它时死锁。

如果你只有几个线程,这些错误可能很明显,因为你的程序将停止正常工作。如果你有一个线程池,根据需要创建更多的线程,那么过时/卡住的线程可能不会被注意到,并将无限期地积累,导致内存泄漏。线程可能会在你的应用程序中使用其他数据,因此也会阻止它们直接引用的任何东西被收集。

举个玩具的例子:

static void leakMe(final Object object) {new Thread() {public void run() {Object o = object;for (;;) {try {sleep(Long.MAX_VALUE);} catch (InterruptedException e) {}}}}.start();}

随意调用System.gc(),但传递给leakMe的对象永远不会死。

不终止的线程(例如在其run方法中无限期休眠)。即使我们丢失了对它的引用,它也不会被垃圾回收。您可以添加字段以使线程对象成为您想要的大对象。

目前的最高答案列出了更多的技巧,但这些似乎是多余的。

关于如何在Java中创建内存泄漏有很多答案,但请注意采访中提出的问题。

“如何使用Java创建内存泄漏?”是一个开放式问题,其目的是评估开发人员的经验程度。

如果我问你“你有Java内存泄漏的故障排除经验吗?”,你的答案将是一个简单的“是”。然后我必须跟进“你能给我举个例子来解决内存泄漏问题吗?”,你会给我一两个例子。

然而,当面试官问“如何使用Java创建内存泄漏?”时,预期的答案应该如下所示:

  • 我遇到了内存泄漏…(说什么时候)[显示我的经验]
  • 导致它的代码是…(解释代码)[你自己修复了它]
  • 我应用的修复是基于…(解释修复)[这让我有机会询问有关修复的细节]
  • 我做的测试是……[让我有机会询问其他测试方法]
  • 我是这样记录的…[加分。如果你记录下来就好了]
  • 因此,我们可以合理地认为,如果我们按照相反的顺序执行此操作,即获取我修复的代码并删除我的修复程序,我们就会出现内存泄漏。

当开发人员未能遵循这一思路时,我试图引导他/她问“你能给我一个Java如何泄漏内存的例子吗?”,然后是“你是否曾经在Java中修复任何内存泄漏?”

请注意,我是没有询问如何在Java中泄漏内存的示例。那太愚蠢了。谁会对能够有效编写泄漏内存代码的开发人员感兴趣?

Java1.6中的String.substring方法会造成内存泄漏。

SubString方法如何在Java中工作-JDK 1.7中修复了内存泄漏

什么是内存泄漏:

  • 它是由bug糟糕的设计。引起的
  • 这是对记忆的浪费。
  • 它会随着时间的推移而变得更糟。
  • 垃圾收集器无法清理它。

典型例子:

对象的缓存是一个很好的起点,可以把事情搞砸。

private static final Map<String, Info> myCache = new HashMap<>();
public void getInfo(String key){// uses cacheInfo info = myCache.get(key);if (info != null) return info;
// if it's not in cache, then fetch it from the databaseinfo = Database.fetch(key);if (info == null) return null;
// and store it in the cachemyCache.put(key, info);return info;}

您的缓存不断增长。很快整个数据库就会被吸入内存。更好的设计使用LRUMap(仅将最近使用的对象保存在缓存中)。

当然,你可以让事情变得更复杂:

  • 使用线程本地结构。
  • 添加更多复杂参考树
  • 第三方图书馆造成的泄漏。

经常发生的事情:

如果此Info对象引用了其他对象,而其他对象又引用了其他对象。在某种程度上,您也可以认为这是某种内存泄漏(由糟糕的设计引起)。

在有自己生命周期的类中粗心地使用非静态内部类。

在Java,非静态内部类和匿名类隐式引用保存到它们的外部类。另一方面,静态内部类不要

以下是Android内存泄漏的一个常见示例,但这并不明显:

public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() { // Non-static inner class, holds the reference to the SampleActivity outer class@Overridepublic void handleMessage(Message msg) {// ...}}
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
// Post a message and delay its execution for a long time.mLeakyHandler.postDelayed(new Runnable() {//here, the anonymous inner class holds the reference to the SampleActivity class too@Overridepublic void run() {//....}}, SOME_TOME_TIME);
// Go back to the previous Activity.finish();}}

这将防止活动上下文被垃圾回收。

Java中没有内存泄漏。内存泄漏是从C等人那里借来的一个短语。Java在GC的帮助下处理内部内存分配。有内存浪费(即留下滞留对象),但没有内存泄漏

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

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

基本上,重要的部分是:

标记和扫描模型

JRockit JVM使用标记和扫描垃圾回收机制模型执行整个堆的垃圾回收。标记和扫描垃圾回收机制包括两个阶段,标记阶段和扫描阶段。

在标记阶段所有可从Java访问的对象线程、本机句柄和其他根源被标记为活动,如以及可以从这些物体到达的物体等等这个过程识别并标记所有仍然存在的对象#36825;,其余的都可以视为垃圾。

在扫描阶段,堆被遍历以找到之间的间隙这些空白被记录在一个自由列表中,并被创建可用于新对象分配。

JRockit JVM使用两个改进版本的标记和扫描模型。一个主要是并发标记和扫描,另一个是并行标记和扫描。您还可以混合这两种策略,运行例如,主要是并发标记和并行扫描。

因此,要在Java中创建内存泄漏;最简单的方法是创建一个数据库连接,做一些工作,只是不Close()它;然后在范围内生成一个新的数据库连接。例如,这在循环中不难做到。如果你有一个从队列中拉取并推送到数据库的工作线程,你可以通过忘记Close()连接或在不必要的时候打开它们来轻松创建内存泄漏,等等。

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

值得注意的是,这适用于所有GC语言。下面是我见过的一些作为SRE工作的示例:

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

面试官可能正在寻找像下面代码一样的循环引用(顺便说一句,这只会在使用引用计数的非常旧的JVM中泄漏内存,现在不再是这种情况了)。但这是一个相当模糊的问题,所以这是展示你对JVM内存管理理解的绝佳机会。

class A {B bRef;}
class B {A aRef;}
public class Main {public static void main(String args[]) {A myA = new A();B myB = new B();myA.bRef = myB;myB.aRef = myA;myA=null;myB=null;/* at this point, there is no access to the myA and myB objects, *//* even though both objects still have active references. */} /* main */}

然后您可以解释使用引用计数时,上述代码会泄漏内存。但大多数现代JVM不再使用引用计数。大多数使用扫描垃圾收集器,它实际上会收集此内存。

接下来,您可以解释创建一个具有底层本机资源的Object,如下所示:

public class Main {public static void main(String args[]) {Socket s = new Socket(InetAddress.getByName("google.com"),80);s=null;/* at this point, because you didn't close the socket properly, *//* you have a leak of a native descriptor, which uses memory. */}}

然后您可以解释这在技术上是内存泄漏,但实际上泄漏是由JVM中分配底层本机资源的本机代码引起的,这些资源没有被您的Java代码释放。

最后,使用现代JVM,您需要编写一些Java代码来分配JVM正常感知范围之外的本机资源。

就像这样!

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

JDK 1.7之前的内存泄漏实时示例:

假设您读取一个包含1000行文本的文件并将它们保存在String对象中:

String fileText = 1000 characters from filefileText = fileText.subString(900, fileText.length());

在上面的代码中,我最初读取1000个字符,然后执行substring以仅获取最后100个字符。现在fileText应该只引用100个字符,所有其他字符都应该被垃圾收集,因为我丢失了引用,但在JDK 1.7之前,substring函数间接引用了最后100个字符的原始字符串,并防止整个字符串的垃圾回收机制,整个1000个字符将在内存中,直到您丢失子字符串的引用。

您可以创建一个内存泄漏示例,如上所述。

创建潜在的巨大内存泄漏的另一种方法是保存对TreeMapMap.Entry<K,V>的引用。

很难评估为什么这只适用于TreeMap,但通过查看实现,原因可能是:TreeMap.Entry存储对其兄弟姐妹的引用,因此如果TreeMap准备好被收集,但其他一些类持有对其Map.Entry中任何一个的引用,那么整个 Map将保留到内存中。


真实场景:

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

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

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

考虑以下包装类:

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 TreeMapIntStream.range(0, 500).forEach(value -> {// create mapfinal Map<String, Integer> map = pseudoQueryDatabase();
final int index = new Random().nextInt(MAP_SIZE);
// get random entry from mapfor (Map.Entry<String, Integer> entry : map.entrySet()) {if (entry.getValue().equals(index)) {holdersCache.add(new EntryHolder(entry));break;}}// to observe behavior in visualvmtry {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()调用之后,map实例应该准备好收集,但它不会发生,因为至少有一个Entry存储在其他地方。

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

您可以从这个visualvm图中看到内存是如何不断增长的。

内存转储-树映射

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

这是使用HashMap时的图表。

内存转储-HashMap

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


我写了一个更广泛的基准测试这里

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

首先需要监控Java内存消耗。

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

jstat -gcutil <process_id> <timeout>

它将报告每一代的内存消耗(年轻老年)和垃圾回收机制时间(年轻充分)。

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

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

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

然后,您需要分析heap.bin文件,例如存储器分析器Eclipse内存分析器(MAT)。

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

内存泄漏是一种资源泄漏,当计算机程序错误地管理内存分配时,内存不再需要不释放=>维基百科定义

这是一个相对基于上下文的主题,你可以根据自己的口味创建一个,只要未使用的引用永远不会被客户使用,但仍然活着。

第一个示例应该是一个没有失效的自定义堆栈,有效Java,项目6中的过时引用。

当然,只要你愿意,还有更多,但如果我们只看Java内置类,它可能是一些

subList()

让我们检查一些超级愚蠢代码来产生泄漏。

public class MemoryLeak {private static final int HUGE_SIZE = 10_000;
public static void main(String... args) {letsLeakNow();}
private static void letsLeakNow() {Map<Integer, Object> leakMap = new HashMap<>();for (int i = 0; i < HUGE_SIZE; ++i) {leakMap.put(i * 2, getListWithRandomNumber());}}


private static List<Integer> getListWithRandomNumber() {List<Integer> originalHugeIntList = new ArrayList<>();for (int i = 0; i < HUGE_SIZE; ++i) {originalHugeIntList.add(new Random().nextInt());}return originalHugeIntList.subList(0, 1);}}

实际上,还有另一个技巧,我们可以通过利用HashMap的查找过程来导致内存泄漏。

  • hashCode()总是相同的,但equals()不同;
  • 使用随机hashCode()equals()始终为真;

为啥?

hashCode()->桶=>equals()来定位货币对


我想先提到#0,然后再提到subList(),但似乎这个问题已经在JDK 8中得到了解决。

public String substring(int beginIndex, int endIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}if (endIndex > value.length) {throw new StringIndexOutOfBoundsException(endIndex);}int subLen = endIndex - beginIndex;if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}return ((beginIndex == 0) && (endIndex == value.length)) ? this: new String(value, beginIndex, subLen);}

Java的内存泄漏示例之一是当忘记调用ResultSets关闭方法时导致的MySQLs内存泄漏bug。例如:

while(true) {ResultSet rs = database.select(query);...// going to next step of loop and leaving resultset without calling rs.close();}
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();}}}

创建一个JNI函数,其中只包含一个whel-true循环,然后用另一个线程中的一个大对象调用它。GC非常不喜欢JNI,并将该对象永远保留在内存中。

Java内存泄漏有很多很好的例子,我将在这个答案中提到其中的两个。

示例1:

下面是本书有效Java,第三版内存泄漏的一个很好的例子(项目7:消除过时的对象引用):

// Can you spot the "memory leak"?public class Stack {private static final int DEFAULT_INITIAL_CAPACITY = 16;private Object[] elements;private int size = 0;
public Stack() {elements = new Object[DEFAULT_INITIAL_CAPACITY];}
public void push(Object e) {ensureCapacity();elements[size++] = e;}
public Object pop() {if (size == 0) throw new EmptyStackException();return elements[--size];}
/*** Ensure space for at least one more element, roughly* doubling the capacity each time the array needs to grow.*/private void ensureCapacity() {if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);}}

这是本书的段落,描述了为什么这个实现会导致内存泄漏:

如果堆栈增长然后收缩,则从堆栈中弹出的对象堆栈不会被垃圾收集,即使程序使用堆栈不再引用它们。这是因为堆栈维护对这些对象的过时引用。一个过时的引用只是一个永远不会被取消引用的引用再一次。在这种情况下,“活动部分”之外的任何引用元素数组已过时。活动部分由索引小于size的元素

以下是本书解决此内存泄漏的解决方案:

解决这类问题的方法很简单:null out引用一旦过时。在我们的Stack类的情况下,对项目的引用在弹出后立即过时从堆栈中删除。pop方法的更正版本如下所示:

public Object pop() {if (size == 0) throw new EmptyStackException();Object result = elements[--size];elements[size] = null; // Eliminate obsolete referencereturn result;}

但是我们如何防止内存泄漏发生呢?这是书中的一个很好的警告:

一般来说,每当一个类管理自己的内存时,程序员应该警惕内存泄漏。每当一个元素被释放时,元素中包含的任何对象引用都应该是已删除

示例2:

观察者模式也可能导致内存泄漏。您可以在以下链接中阅读有关此模式的信息:观察者模式

这是观察者模式的一个实现:

class EventSource {public interface Observer {void update(String event);}
private final List<Observer> observers = new ArrayList<>();
private void notifyObservers(String event) {observers.forEach(observer -> observer.update(event)); //alternative lambda expression: observers.forEach(Observer::update);}
public void addObserver(Observer observer) {observers.add(observer);}
public void scanSystemIn() {Scanner scanner = new Scanner(System.in);while (scanner.hasNextLine()) {String line = scanner.nextLine();notifyObservers(line);}}}

在这个实现中,EventSource,在观察者设计模式中是Observable,可以保存到Observer对象的链接,但是这个链接永远不会从EventSource中的observers字段中删除。所以它们永远不会被垃圾收集器收集。解决这个问题的一个解决方案是向客户端提供另一种方法,当他们不再需要上述观察者时,从observers字段中删除这些观察者:

public void removeObserver(Observer observer) {observers.remove(observer);}

对以前的答案(为了更快地生成内存泄漏)的一点改进是使用从大XML文件加载的DOM Document实例。

这很简单:

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

您可以尝试让许多缓冲的阅读器尝试使用while循环同时打开同一个文件,条件永远不会为假。最重要的是这些永远不会关闭。