题 使用Java创建内存泄漏


我刚接受采访,并被要求用Java创建内存泄漏。 毋庸置疑,我觉得自己很傻,甚至不知道如何开始创建一个。

一个例子是什么?


2641


起源


我会告诉他们Java使用垃圾收集器,并要求他们更加具体地说明他们对“内存泄漏”的定义,并解释说 - 除了JVM错误 - Java不会泄漏内存 相当 与C / C ++相同的方式。您必须具有对该对象的引用 某处。 - Darien
我觉得有趣的是,在大多数答案中,人们正在寻找那些边缘案例和技巧,似乎完全忽略了这一点(IMO)。他们可以只显示对无法再次使用的对象进行无用引用的代码,同时永远不会丢弃这些引用;可能会说这些情况不是“真正的”内存泄漏,因为仍有对这些对象的引用,但如果程序从不再使用这些引用并且也从不丢弃它们,那么它完全等同于(并且一样糟糕)“真正的内存泄漏“。 - ehabkost
老实说,我不敢相信我问过的关于“Go”的类似问题被归结为-1。这里: stackoverflow.com/questions/4400311/...   基本上我正在谈论的内存泄漏是那些得到+200投票给OP的人,然而我受到了攻击和侮辱,询问“Go”是否有同样的问题。不知怎的,我不确定所有的维基都是如此的好。 - SyntaxT3rr0r
@ SyntaxT3rr0r - darien的答案不是狂热主义。他明确承认某些JVM可能存在意味着内存泄露的错误。这与语言规范本身不同,允许内存泄漏。 - Peter Recore
@ehabkost:不,他们不等同。 (1) 你有能力回收内存,而在“真正的泄漏”你的C / C ++程序忘记分配的范围,没有安全的方法来恢复。 (2) 您可以通过分析很容易地检测到问题,因为您可以看到“膨胀”涉及的对象。 (3) “真正的泄漏”是一个明确的错误,而一个程序可以保留大量的对象直到终止 可以 是故意如何工作的一个故意的一部分。 - Darien


答案:


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

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

这是有效的,因为ThreadLocal保留对该对象的引用,该对象保持对其Class的引用,而Class又保持对其ClassLoader的引用。反过来,ClassLoader保持对它已加载的所有类的引用。

(在许多JVM实现中,尤其是在Java 7之前,情况更糟,因为Classes和ClassLoader直接分配到permgen并且根本就没有GC。但是,无论JVM如何处理类卸载,ThreadLocal仍然会阻止被回收的类对象。)

此模式的一个变体是,如果您经常重新部署碰巧以任何方式使用ThreadLocals的应用程序,那么应用程序容器(如Tomcat)可能会像筛子一样泄漏内存。 (由于应用程序容器使用了所描述的线程,每次重新部署应用程序时都会使用新的ClassLoader。)

更新:因为很多人一直在要求它, 这是一些显示此行为的示例代码


1935



+1 ClassLoader泄漏是JEE世界中最常见的一些内存泄漏,通常是由转换数据的第三方库(BeanUtils,XML / JSON编解码器)引起的。当lib被加载到应用程序的根类加载器之外但保存对类的引用(例如通过缓存)时,可能会发生这种情况。当您取消部署/重新部署应用程序时,JVM无法垃圾收集应用程序的类加载器(以及因此加载的所有类),因此重复部署应用程序服务器最终会出现问题。如果幸运的话,你得到了一个关于ClassCastException的线索z.x.y.Abc无法转换为z.x.y.Abc - earcam
tomcat在所有加载的类中使用了tricks和nils ALL静态变量,tomcat虽然有很多数据集和错误的编码(需要一些时间并提交修复),加上所有令人难以置信的ConcurrentLinkedQueue作为内部(小)对象的缓存,如此小,以至于ConcurrentLinkedQueue.Node占用更多内存。 - bestsss
+1:Classloader泄漏是一场噩梦。我花了几周的时间试图解决这些问题。令人遗憾的是,正如@earcam所说,它们主要是由第三方库引起的,而且大多数剖析器都无法检测到这些泄漏。在这个博客上有关于Classloader泄漏的清晰解释。 blogs.oracle.com/fkieviet/entry/... - Adrian M
@Nicolas:你确定吗? JRockit默认执行GC类对象,而HotSpot则不执行,但AFAIK JRockit仍然不能将GC作为ThreadLocal引用的Class或ClassLoader。 - Daniel Pryden
Tomcat将尝试为您检测这些泄漏,并警告它们: wiki.apache.org/tomcat/MemoryLeakProtection。最新版本有时甚至会为您修复泄漏。 - Matthijs Bierman


静态字段保持对象引用[esp final field]

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

调用 String.intern() 在冗长的字符串上

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(未封闭的)开放流(文件,网络等......)

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选项不正确或不适当, 如那个 noclassgc IBM JDK上的选项,用于防止未使用的类垃圾回收

看到 IBM jdk设置


1062



我不同意上下文和会话属性是“泄漏”。它们只是长期存在的变量。静态最终字段或多或少只是一个常数。也许应避免使用大常量,但我认为将其称为内存泄漏是不公平的。 - Ian McLaird
(未封闭的)开放流(文件,网络等......),在最终确定期间(将在下一个GC周期之后)实际上不泄漏close()将被安排(close() 通常不会在终结器线程中调用,因为可能是阻塞操作)。不关闭是一种不好的做法,但它不会导致泄漏。未公开的java.sql.Connection是相同的。 - bestsss
在大多数理智的JVM中,似乎String类只有一个弱引用 intern 哈希表内容。就这样,它 是 垃圾收集得当,没有泄漏。 (但是IANAJP) mindprod.com/jgloss/interned.html#GC - Matt B.
静态字段持有对象引用[esp final field]如何是内存泄漏? - Kanagavelu Sugumar
@cHao True。我遇到的危险不是来自Streams泄露的内存问题。问题是没有足够的内存泄漏。你可以泄漏很多手柄,但仍然有足够的内存。然后,垃圾收集器可能决定不打算进行完整的收集,因为它仍然有足够的内存。这意味着不会调用终结器,因此您的句柄用完了。问题是,终结器将(通常)在泄漏流耗尽内存之前运行,但在用完内存之外的其他内容之前可能不会调用它。 - Patrick M


一个简单的事情是使用不正确(或不存在)的HashSet hashCode() 要么 equals(),然后继续添加“重复”。而不是忽略重复,它只会增长,你将无法删除它们。

如果你想让这些坏键/元素闲逛,你可以使用静态字段

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.

390



实际上,即使元素类获得hashCode并且等于错误,您也可以从HashSet中删除元素;只需获取集合的迭代器并使用其remove方法,因为迭代器实际上对底层条目本身而不是元​​素进行操作。 (注意,未实现的hashCode / equals是 不 足以引发泄漏;默认值实现简单的对象标识,因此您可以获取元素并正常删除它们。) - Donal Fellows
@Donal我想说的是,我猜,我不同意你对内存泄漏的定义。我会考虑(继续类比)你的迭代器去除技术是泄漏下的滴水盘;无论滴水盘如何,泄漏仍然存在。 - corsiKa
我同意,这是 不 内存“泄漏”,因为您可以删除对hashset的引用并等待GC启动,并且presto!记忆又回来了。 - Mehrdad
@ SyntaxT3rr0r,我将你的问题解释为询问语言中是否有任何自然导致内存泄漏的内容。答案是不。这个问题询问是否有可能设法创建类似内存泄漏的情况。这些示例都不是C / C ++程序员理解它的方式的内存泄漏。 - Peter Lawrey
@Peter Lawrey:还有,你怎么看待这个: “如果您不忘记手动释放分配的内存,C语言中没有任何内容会自然泄漏到内存泄漏中”。这对智力不诚实有什么影响?无论如何,我很累:你可以说最后一句话。 - SyntaxT3rr0r


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

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

我将专注于线程,以显示大多数非托管线程的危险,不希望甚至触摸挥杆。

  • Runtime.addShutdownHook 而不是删除...然后甚至使用removeShutdownHook由于ThreadGroup类中有关未启动线程的错误,它可能无法收集,有效地泄漏了ThreadGroup。 JGroup在GossipRouter中有漏洞。

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

  • 创建一个线程继承了 ContextClassLoader 和 AccessControlContext加上 ThreadGroup 和任何 InheritedThreadLocal,所有这些引用都是潜在的泄漏,以及类加载器和所有静态引用加载的整个类,以及ja-ja。整个j.u.c.Executor框架具有超级简单的效果,尤其明显 ThreadFactory 界面,但大多数开发人员都不知道潜伏的危险。此外,许多库都会根据请求启动线程(太多行业流行的库)。

  • ThreadLocal 高速缓存;那些在许多情况下是邪恶的。我确信每个人都已经看到了很多基于ThreadLocal的简单缓存,这也是坏消息:如果线程持续超过预期生命的上下文ClassLoader,那么这是一个纯粹的小漏洞。除非确实需要,否则不要使用ThreadLocal缓存。

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

  • 使用WeakHashMap和值(in)直接引用键。没有堆转储,这是一个很难找到的。这适用于所有扩展 Weak/SoftReference 这可能会保留一个硬参考回到受保护的对象。

  • 运用 java.net.URL 使用HTTP(S)协议并从(!)加载资源。这个是特别的, KeepAliveCache 在系统ThreadGroup中创建一个新线程,它泄漏当前线程的上下文类加载器。当没有活动线程存在时,线程是在第一个请求时创建的,所以你可能会幸运或只是泄漏。 泄漏已经在Java 7中得到修复,并且创建线程的代码正确地删除了上下文类加载器。 还有更多的案例(喜欢ImageFetcher也固定了)创建类似的线程。

  • 运用 InflaterInputStream 通过 new java.util.zip.Inflater() 在构造函数中(PNGImageDecoder 例如)而不是打电话 end() inflater。好吧,如果你只传入构造函数 new,没有机会......是的,打电话 close() 如果它作为构造函数参数手动传递,则在流上不会关闭inflater。这不是真正的泄漏,因为它会被终结者释放......当它认为有必要时。直到那一刻它吃掉本机内存如此糟糕,它可能导致Linux oom_killer肆无忌惮地杀死进程。主要问题是Java的最终确定非常不可靠,G1在7.0.2之前变得更糟。故事的道德:尽快释放本地资源;终结者太穷了。

  • 同样的情况 java.util.zip.Deflater。这个更糟糕,因为Deflater在Java中需要内存,即总是使用15位(最大值)和8个内存级别(最多9个)来分配几百KB的本机内存。幸好, Deflater 没有广泛使用,据我所知JDK不包含任何错误。总是打电话 end() 如果你手动创建一个 Deflater 要么 Inflater。最后两个中最好的部分: 您无法通过常规的分析工具找到它们。

(我可以根据要求添加更多时间浪费。)

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


228



Creating but not starting a Thread... 哎呀,几个世纪前我被这个人严重咬了! (Java 1.3) - leonbloy
@leonbloy,在线程被直接添加到线程组之前更糟糕的是,没有启动意味着非常难以泄漏。不只是增加了 unstarted 计数,但这可以防止线程组破坏(较小的邪恶,但仍然是泄漏) - bestsss
谢谢! “叫 ThreadGroup.destroy() 当ThreadGroup本身没有线程时...“ 是一个令人难以置信的微妙的错误;我已经追了好几个小时了,因为在我的控制界面中枚举线程没有显示任何内容,导致误入歧途,但线程组和大概至少一个子组不会消失。 - Lawrence Dol
@bestsss:我很好奇,为什么你要删除一个关闭钩子,因为它运行在,JVM关闭? - Lawrence Dol


这里的大多数例子都“过于复杂”。他们是边缘案件。使用这些示例,程序员犯了一个错误(比如不重新定义equals / hashcode),或者被JVM / JAVA的一个角落案例(带有静态的类加载......)所困扰。我认为这不是面试官想要的例子,甚至是最常见的案例。

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

但任何长期存在的应用程序都倾向于共享状态。它可以是任何东西,静力学,单身......通常非平凡的应用程序往往会制作复杂的对象图。只是忘记设置null或更多的引用经常忘记从集合中删除一个对象足以导致内存泄漏。

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

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

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

内存泄漏确实是可能的,并且是完全可预测的。无需特殊语言功能或角落案例。内存泄漏可能表明某些内容可能缺失甚至是设计问题。


150



我觉得很有趣的是,在其他答案中,人们正在寻找那些边缘案例和技巧,似乎完全忽略了这一点。他们可以只显示对永远不会再使用的对象进行无用引用的代码,并且永远不会删除这些引用;可能会说这些情况不是“真正的”内存泄漏,因为仍有对这些对象的引用,但如果程序从不再使用这些引用并且也从不丢弃它们,那么它完全等同于(并且一样糟糕)“真正的内存泄漏“。 - ehabkost
@Nicolas Bousquet: “内存泄漏确实很可能”   非常感谢。 +15支持。尼斯。我在这里大吼大叫说出这个事实,作为关于Go语言的问题的前提: stackoverflow.com/questions/4400311   这个问题仍然有负面的投票:( - SyntaxT3rr0r
Java和.NET中的GC在某种意义上是基于假设图,其中包含对其他对象的引用的对象与“关心”其他对象的对象图相同。实际上,边缘可能存在于参考图中而不表示“关怀”关系,并且即使没有直接或间接的参考路径,对象也可能关心另一个对象的存在(即使使用 WeakReference)从一个存在到另一个。如果对象引用有一个备用位,那么“关注目标”指示器可能会有所帮助...... - supercat
...并让系统提供通知(通过类似的方式) PhantomReference如果发现一个物体没有任何人关心它。 WeakReference 有点接近,但必须先转换为强引用才能使用;如果在强引用存在的情况下发生GC循环,则假定目标有用。 - supercat
这是我认为正确的答案。我们几年前写了一个模拟。不知何故,我们意外地将先前状态与当前状态相关联,从而造成内存泄漏由于截止日期,我们从未解决内存泄漏,但通过记录它使其成为“功能”。 - nalply


答案完全取决于面试官的想法。

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

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

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

我正在读你的元问题:“在这次面试中我能用到什么答案”。因此,我将专注于面试技巧而不是Java。我相信你更有可能在面试中重复不知道问题答案的情况,而不是在需要知道如何使Java泄漏的地方。所以,希望这会有所帮助。

您可以为面试开发的最重要的技能之一是学习积极倾听问题并与面试官合作以提取他们的意图。这不仅可以让你以他们想要的方式回答他们的问题,而且还表明你有一些重要的沟通技巧。当谈到许多同样有才华的开发人员之间的选择时,我会聘请那些在每次回复之前倾听,思考和理解的人。


133



每当我提出这个问题时,我都在寻找一个非常简单的答案 - 继续增长队列,没有最终关闭数据库等,而不是奇怪的类加载器/线程细节,这意味着他们理解gc可以和不能为你做什么。取决于你正在面试的工作我猜。 - DaveC
请看看我的问题,谢谢 stackoverflow.com/questions/31108772/... - Daniel Newtown


如果你不明白,以下是一个非常毫无意义的例子 JDBC。或者至少JDBC希望开发人员关闭的方式 ConnectionStatement 和 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 对象未关闭,因此物理连接将保持打开状态,直到垃圾收集器出现并看到它无法访问。 GC会调用 finalize 方法,但有没有实现的JDBC驱动程序 finalize,至少不是这样的 Connection.close 已实施。由此产生的行为是,由于收集了无法访问的对象,将回收内存,与之关联的资源(包括内存) Connection 对象可能根本无法回收。

在这样的情况下 Connectionfinalize 方法不会清理所有内容,实际上可能会发现与数据库服务器的物理连接将持续几个垃圾收集周期,直到数据库服务器最终确定连接不存在(如果存在),并且应该关闭。

即使JDBC驱动程序要实现 finalize,有可能在最终确定期间抛出异常。由此产生的行为是,与现在“休眠”对象关联的任何内存都不会被回收,如 finalize 保证只被调用一次。

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

你可以想出更多的例子 - 比如

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

113



在达到内存限制之前,您将达到最大打开连接限制。不要问我为什么知道...... - Hardwareguy
Oracle JDBC驱动程序因此而臭名昭着。 - chotchki
@Hardwareguy在我提出之前,我经常达到SQL数据库的连接限制 Connection.close 进入我所有SQL调用的finally块。为了获得额外的乐趣,我调用了一些长期运行的Oracle存储过程,这些过程需要Java端的锁定来防止对数据库的过多调用。 - Michael Shopsin
@Hardwareguy这很有意思,但实际的连接限制并不一定适用于所有环境。例如,对于部署在weblogic app server 11g上的应用程序,我已经看到大规模的连接泄漏。但由于可以选择收获泄漏的连接,因此在引入内存泄漏时仍可使用数据库连接。我不确定所有环境。 - Aseem Bansal
根据我的经验,即使关闭连接,也会出现内存泄漏。您需要先关闭ResultSet和PreparedStatement。由于OutOfMemoryErrors,服务器在经过数小时甚至数天的工作后反复崩溃,直到我开始这样做。 - Bjørn Stenfeldt