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


3221

我刚刚接受采访,并被要求使用Java 造成内存泄漏
不用说,我对如何开始创建它一无所知。

一个例子是什么?


275
我会告诉他们,Java使用垃圾收集,并请他们对自己的“内存泄漏”的定义多一点具体的,并解释说-除非JVM的错误- Java可以在不泄露内存颇为相同的方式Ç / C ++可以。您必须在某处引用该对象。
达里安(Darien)

371
我觉得很有趣,在大多数答案中,人们都在寻找那些极端的案例和技巧,并且似乎完全没有抓住重点(IMO)。它们可能只显示代码,这些代码保留了对不再使用的对象的无用引用,同时也从不丢弃这些引用;可能会说这些情况不是“真正的”内存泄漏,因为周围仍然有对这些对象的引用,但是如果程序从不再次使用这些引用,也从不丢弃它们,则它完全等同于(并且同样糟糕)真正的内存泄漏”。
ehabkost,2011年

62
老实说,我不敢相信我问“ Go”的类似问题被减为-1。在这里:stackoverflow.com/questions/4400311/… 基本上,我所谈论的内存泄漏是那些向OP投票+200的内存泄漏,但是我遭到攻击和侮辱,询问“ Go”是否存在相同的问题。不知道为什么我不确定所有的wiki东西都能正常工作。
语法T3rr0r,2011年

74
@ SyntaxT3rr0r-达里恩的答案不是狂热。他明确承认某些JVM可能存在一些错误,这些错误意味着内存泄漏。这与允许内存泄漏的语言规范本身不同。
Peter Recore 2011年

31
@ehabkost:不,它们不是等效的。(1)您具有回收内存的能力,而在“真正的泄漏”中,您的C / C ++程序会忘记分配的范围,因此没有安全的恢复方法。(2)您可以很容易地检测到分析问题,因为您可以看到“膨胀”涉及的对象。(3) “真正的泄漏”是一个明确的错误,而将许多对象保留到终止之前的程序可能是其工作方式的有意部分。
达里安(Darien)

Answers:


2309

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

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

由于该方法ThreadLocal是在Oracle JDK中实现的,因此会造成内存泄漏:

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

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

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

ClassLoader在创建泄漏中并没有真正发挥作用,它只是由于以下附加引用链而使泄漏更糟:示例类→→ ClassLoader它已加载的所有类。在许多JVM实现中,甚至在更糟糕的情况下,情况甚至更糟。 Java 7,因为类和ClassLoaders被直接分配到permgen中,并且根本不会被垃圾回收。)

这种模式的一个变种是,如果您频繁地重新部署碰巧使用ThreadLocals的应用程序,而应用程序容器(例如Tomcat)像sieve 那样以某种方式指向自身,那么它可以像筛子一样泄漏内存。发生这种情况的原因很多,通常很难调试和/或修复。

更新:由于很多人一直在要求它,因此以下示例代码展示了这种行为


186
+1 ClassLoader泄漏是JEE世界中一些最常见的内存泄漏,通常是由转换数据的第三方库(BeanUtils,XML / JSON编解码器)引起的。当将lib加载到应用程序的根类加载器之外但保留对类的引用(例如,通过缓存)时,可能会发生这种情况。取消部署/重新部署应用程序时,JVM无法垃圾回收应用程序的类加载器(以及由此加载的所有类),因此重复进行部署会使应用程序服务器最终陷入困境。如果幸运的话,您将获得有关ClassCastException的线索zxyAbc无法转换为zxyAbc
earcam 2011年

7
tomcat使用技巧,并在所有已加载的类中钉住所有静态变量,尽管tomcat有很多数据争用和错误的编码(需要花费一些时间并提交修复程序),再加上令人难以置信的ConcurrentLinkedQueue作为内部(小)对象的缓存,如此之小,甚至ConcurrentLinkedQueue.Node也占用更多内存。
bestsss 2011年

57
+1:Classloader泄漏是一场噩梦。我花了数周的时间试图弄清楚它们。可悲的是,就像@earcam所说的那样,它们主要是由第三方库引起的,而且大多数分析器也无法检测到这些泄漏。这个博客上有一个关于Classloader泄漏的清晰明了的解释。blogs.oracle.com/fkieviet/entry/...
阿德里安中号

4
@Nicolas:你确定吗?JRockit默认情况下会处理GC类对象,而HotSpot不会,但AFAIK JRockit仍无法对ThreadLocal引用的类或ClassLoader进行GC。
Daniel Pryden 2011年

6
Tomcat将尝试为您检测这些泄漏,并对其进行警告:wiki.apache.org/tomcat/MemoryLeakProtection。最新版本有时甚至可以为您解决漏洞。
Matthijs Bierman'3

1212

静态字段保存对象参考[特别是最终字段]

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选项,例如noclassgcIBM JDK上的选项,可防止未使用的类垃圾回收

请参阅IBM jdk设置


178
我不同意上下文和会话属性是“泄漏”。它们只是长期存在的变量。静态最终字段或多或少只是一个常数。也许应该避免使用大常量,但我认为称其为内存泄漏并不公平。
伊恩·麦克劳德

80
(未关闭的)打开的流(文件,网络等),不会真正泄漏,在完成期间(将在下一个GC周期之后)将被安排close()(close()通常不会在终结器中调用)线程,因为它可能是阻塞操作)。不关闭是一种不好的做法,但不会造成泄漏。未关闭的java.sql.Connection相同。
bestsss 2011年

33
在大多数理智的JVM中,似乎String类在其intern哈希表内容上仅具有弱引用。因此,它正确收集的垃圾,而不是泄漏。(但IANAJP)mindprod.com/jgloss/interned.html#GC
马特B.

42
静态字段保存对象引用[esp final field]如何导致内存泄漏?
Kanagavelu Sugumar 2012年

5
@cHao是的。我遇到的危险不是Streams泄漏内存的问题。问题在于它们没有泄漏足够的内存。您可以泄漏很多句柄,但仍有大量内存。然后,垃圾收集器可能会决定不打扰进行完整的收集,因为它仍然有足够的内存。这意味着没有调用终结器,因此用尽了句柄。问题在于,终结器(通常)会在您由于泄漏的流而用完内存之前运行,但是在耗尽内存以外的内容之前可能不会调用终结器。
Patrick M

460

一个简单的事情是使用带有错误(或不存在)hashCode()或的HashSet 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.

68
实际上,即使元素类获得hashCode并等于错误,也可以从HashSet中删除元素。只需为集合获取一个迭代器并使用其remove方法,因为迭代器实际上是对基础条目本身而不是元​​素进行操作。(请注意,未实现的hashCode /等于是不是足以引发泄漏;默认实现简单的对象的身份,所以你能得到的元素,通常将其删除。)
多纳尔研究员

12
@Donal,我想说的是我不同意您对内存泄漏的定义。我会认为(继续类推)您的迭代器删除技术是泄漏下的“滴水锅”。无论滴盘如何,泄漏点仍然存在。
corsiKa 2011年

94
我同意,这不是内存“泄漏”,因为您可以删除对哈希集的引用,然后等待GC启动,然后保存!记忆回到过去。
2011年

12
@ SyntaxT3rr0r,我将您的问题解释为询问该语言中是否存在自然导致内存泄漏的任何内容。答案是不。这个问题问是否有可能制造某种情况来产生类似内存泄漏的情况。这些示例都不是C / C ++程序员理解的方式的内存泄漏。
彼得·劳瑞

11
@Peter Lawrey:另外,您对此有何看法:“如果您不忘记手动释放分配的内存,那么C语言中不会有任何自然泄漏到内存泄漏的东西”。那会怎样导致思想上的不诚实?无论如何,我很累:您可以说最后一句话。
语法T3rr0r 2011年

271

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

  • File.deleteOnExit() -总是泄漏字符串, 如果字符串是子字符串,则泄漏会更加严重(底层的char []也被泄漏)- 在Java 7中,子字符串也复制char[],因此后面的内容不适用;@Daniel,不过不需要投票。

我将集中讨论线程,以大体上显示非托管线程的危险,甚至不希望摆动。

  • Runtime.addShutdownHook而不是删除...,即使由于ThreadGroup类中与未启动的线程有关的错误,即使使用removeShutdownHook,也可能无法收集未启动的线程,这实际上会泄漏ThreadGroup。JGroup在GossipRouter中泄漏。

  • 创建(但不开始)Thread与上述类别相同。

  • 创建线程继承了ContextClassLoaderand和AccessControlContextplus ThreadGroup以及any InheritedThreadLocal,所有这些引用都是潜在的泄漏,包括类加载器加载的整个类,所有静态引用以及ja-ja。在具有超简单ThreadFactory界面的整个jucExecutor框架中,这种效果尤其明显,但是大多数开发人员都不知道潜伏的危险。另外,很多库都根据请求启动线程(太多了行业流行的库)。

  • 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()充气机。好吧,如果您用just传递构造函数new,则没有机会...而且,是的,close()如果手动将其作为构造函数参数传递,则调用流不会关闭充气机。这不是真正的泄漏,因为它将由终结器释放……在其认为必要时。直到那一刻,它吞噬了本机内存,严重到可能导致Linux oom_killer毫无惩罚地杀死进程。主要问题是Java中的终结处理非常不可靠,G1恶化到7.0.2。故事的寓意:尽快释放本机资源;终结器太差了。

  • 与相同的情况java.util.zip.Deflater。由于Deflater在Java中占用大量内存,因此这一情况要差得多,即,始终使用15位(最大)和8个内存级别(最大9个)分配数百KB的本机内存。幸运的是,Deflater它没有被广泛使用,据我所知,JDK不包含任何误用。end()如果您手动创建Deflater或,请始终致电Inflater。最后两个最好的部分:您无法通过可用的常规配置工具找到它们。

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

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


23
Creating but not starting a Thread...耶克斯,几个世纪前,我被这个咬了!(Java 1.3)
leonbloy 2011年

@leonbloy,在将线程直接添加到线程组之前甚至更糟之前,没有启动就意味着非常困难的泄漏。不仅增加了unstarted计数,还防止了线程组的破坏(邪恶程度较小,但仍然是泄漏)
致谢

谢谢!ThreadGroup.destroy()当ThreadGroup本身没有线程时调用...”是一个非常微妙的错误;我一直在追赶这个问题达数小时之久,误入歧途,因为枚举我的控制GUI中的线程没有显示任何内容,但是线程组以及大概至少一个子组不会消失。
劳伦斯·多尔

1
@bestsss:我很好奇,为什么要删除关闭挂钩,因为它可以在JVM关闭的情况下运行?
劳伦斯·多尔

203

这里的大多数示例都是“太复杂”。他们是极端情况。在这些示例中,程序员犯了一个错误(例如,不重新定义equals / hashcode),或者被JVM / JAVA的一个极端案例(带有静态类的负载...)所困扰。我认为这不是面试官想要的例子,甚至不是最常见的情况。

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

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

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

我们可能有一个复杂的图,它存储了计算所需的先前状态。但是以前的状态本身链接到之前的状态,依此类推。

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

确实确实有可能发生内存泄漏,并且完全可以预测。无需特殊的语言功能或特殊情况。内存泄漏要么表明某些东西可能丢失,要么表明设计问题。


24
我觉得很有趣,在其他答案上,人们正在寻找那些极端的案例和技巧,似乎完全没有抓住重点。他们可能只显示代码,这些代码保留了对不再使用的对象的无用引用,并且从不删除这些引用;可能会说这些情况不是“真正的”内存泄漏,因为周围仍然有对这些对象的引用,但是如果程序从不再次使用这些引用,也从不丢弃它们,则它完全等同于(并且同样糟糕)真正的内存泄漏”。
ehabkost,2011年

@Nicolas Bousquet:“确实确实有可能发生内存泄漏” ,非常感谢。+15赞。真好 我在这里大声疾呼说出这一事实,作为有关Go语言的前提:stackoverflow.com/questions/4400311 这个问题仍然有负面意见:(
SyntaxT3rr0r 2011年

从某种意义上说,Java和.NET中的GC都是基于持有对其他对象的引用的对象的假设图与“关心”其他对象的对象的图相同的。实际上,参考图中可能存在不表示“关怀”关系的边,即使没有直接或间接的参考路径(即使使用WeakReference)也可能使对象关心另一个对象的存在。从一个到另一个。如果对象引用有多余的余地,那么有一个“关心目标”指示符可能会有所帮助...
supercat

...并让系统提供通知(通过类似的手段PhantomReference),如果发现某个对象没有任何人关心它。 WeakReference有点接近,但是必须先转换为强引用,然后才能使用;如果在存在强参考的同时发生GC周期,则该目标将被认为是有用的。
超级猫

我认为这是正确的答案。我们几年前写了一个模拟。我们不知何故不小心将先前的状态链接到当前状态,从而造成内存泄漏。由于期限的限制,我们从未解决内存泄漏问题,而是通过记录将其作为“功能”。
nalply

163

答案完全取决于访问者认为他们要问的内容。

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

但是可能会问多个元问题?

  • 理论上“完美”的Java实现是否容易受到泄漏的影响?
  • 候选人是否了解理论与现实之间的区别?
  • 候选人是否了解垃圾收集的工作原理?
  • 还是应该在理想情况下进行垃圾收集?
  • 他们是否知道可以通过本机界面调用其他语言?
  • 他们知道以其他语言泄漏内存吗?
  • 候选人是否还知道什么是内存管理以及Java幕后发生了什么?

我正在将您的元问题阅读为“在这种采访情况下我可以使用的答案是什么”。因此,我将专注于面试技巧而不是Java。我相信您比需要知道如何使Java泄漏的地方更容易重复这种情况,即在面试中不知道问题的答案。因此,希望这会有所帮助。

您可以为面试开发的最重要技能之一是学会积极地聆听问题并与面试官一起提取意图。这不仅使您能够以他们想要的方式回答他们的问题,而且还表明您具备一些至关重要的沟通技巧。当涉及到许多同样才华横溢的开发人员之间的选择时,我会雇用一名在每次响应之前都会倾听,思考和理解的人。


22
每当我问这个问题时,我都在寻找一个非常简单的答案-继续增加队列,最后关闭数据库等,而不是奇怪的类加载器/线程详细信息,这意味着他们了解gc可以为您做什么和不能为您做什么。我想这取决于您要面试的工作。
DaveC


130

如果您不了解JDBC,那么以下是一个毫无意义的示例。或者至少是JDBC期望开发人员关闭ConnectionStatementResultSet在丢弃实例或丢失对它们的引用之前关闭实例,而不是依赖的实现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应用程序时不卸载Singleton。显然,加载单例类的Classloader将保留对该类的引用,因此单例实例将永远不会被收集。部署应用程序的新实例时,通常会创建一个新的类加载器,并且由于单例的原因,以前的类加载器将继续存在。

98
通常,在达到内存限制之前,您将达到最大打开连接限制。不要问我为什么我知道...
Hardwareguy

Oracle JDBC驱动程序为此而臭名昭著。
chotchki

@Hardwareguy在达到我Connection.close所有SQL调用的finally块之前,我经常达到SQL数据库的连接限制。为了获得更多乐趣,我调用了一些长时间运行的Oracle存储过程,这些存储过程要求Java端具有锁,以防止对数据库的过多调用。
Michael Shopsin 2014年

@Hardwareguy这很有趣,但是并不一定要在所有环境中都达到实际的连接限制。例如,对于部署在weblogic应用服务器11g上的应用程序,我已经看到了大规模的连接泄漏。但是由于可以选择收集泄漏的连接,因此在引入内存泄漏时数据库连接仍然可用。我不确定所有环境。
Aseem Bansal 2014年

以我的经验,即使关闭连接也会出现内存泄漏。您需要先关闭ResultSet和PreparedStatement。如果服务器由于OutOfMemoryErrors而在数小时甚至数天的正常运行后反复崩溃,直到我开始这样做。
比约恩·斯滕费尔德(BjørnStenfeldt)

119

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)?该引用可能会使一个巨大的对象保持活动状态...


5
内存泄漏在哪里?
rds

28
@maniek:我并不是要暗示此代码存在内存泄漏。我引用它来表明有时需要使用非显而易见的代码来避免意外保留对象。
meriton

什么是RangeCheck(index); ?
Koray Tugay 2014年

6
约书亚·布洛赫(Joshua Bloch)在“有效Java”中给出了此示例,该示例显示了Stacks的简单实现。一个很好的答案。
租用

但这即使忘记了也不是真正的内存泄漏。该元素仍然可以通过Reflection进行SAFELLY访问,只是不明显并且可以通过List接口直接访问,但是对象和引用仍然存在,并且可以安全地访问。
DGoiko

68

每当您保留对不再需要的对象的引用时,就会发生内存泄漏。请参阅处理Java程序中的内存泄漏以获取有关内存泄漏如何在Java中表现出来以及如何处理的示例。


14
我不认为这是“泄漏”。这是一个错误,它是由程序和语言设计的。泄漏将是一个没有任何引用的对象。
user541686'7

29
@Mehrdad:那只是一个狭窄的定义,并不完全适用于所有语言。我认为任何内存泄漏都是由于程序设计不当造成的错误。
比尔蜥蜴

9
@Mehrdad:...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. 我不明白你是如何得出这个结论的。有较少的方式通过任何定义创建Java中的内存泄漏。这绝对仍然是一个有效的问题。
比尔蜥蜴

7
@ 31eee384:如果您的程序将对象保留在它永远无法使用的内存中,那么从技术上讲它是内存泄漏。您遇到更大的问题并不能真正改变这一事实。
比尔蜥蜴

8
@ 31eee384:如果您知道事实并非如此,那就不可能了。按照编写的程序,将永远不会访问数据。
比尔蜥蜴

51

您可以使用sun.misc.Unsafe类使内存泄漏。实际上,此服务类在不同的标准类中使用(例如,在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();
        }
    }

}

1
对于垃圾收集器来说,分配的内存是不可见的

4
分配的内存也不属于Java。
bestsss 2011年

这是sun / oracle jvm特有的吗?例如,这可以在IBM上使用吗?
柏林布朗

2
内存肯定是“属于Java”的,至少在某种意义上说:i)它对其他任何人都不可用; ii)当Java应用程序退出时,它将返回给系统。它就在JVM之外。
Michael Anderson

3
这将在eclipse中构建(至少在最新版本中可用),但是您需要更改编译器设置:在“窗口”>“偏好设置”>“ Java”>“编译器”>“错误/警告”>“弃用和受限制的API”中,将“禁止引用(访问规则)”设置为“警告” ”。
Michael Anderson

43

我可以从这里复制答案: 导致Java内存泄漏的最简单方法?

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

简单的答案是:您不能。Java会执行自动内存管理,并将释放您不需要的资源。您无法阻止这种情况的发生。它将始终能够释放资源。在具有手动内存管理功能的程序中,这是不同的。您无法使用malloc()在C中获得一些内存。要释放内存,您需要malloc返回的指针并对其调用free()。但是,如果您再也没有指针(被覆盖或超过了生存期),那么很遗憾,您将无法释放此内存,从而导致内存泄漏。

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

长答案是:您可以通过使用JNI编写Java库来获得内存泄漏,该库可以进行手动内存管理,从而导致内存泄漏。如果调用此库,则Java进程将泄漏内存。或者,您可以在JVM中出现错误,以便JVM释放内存。JVM中可能存在错误,甚至可能有一些已知的错误,因为垃圾回收并不是那么简单,但是那仍然是一个错误。通过设计,这是不可能的。您可能会要求一些受此类错误影响的Java代码。抱歉,我不知道,在下一个Java版本中也可能不再是bug。


12
那是对内存泄漏的极其有限的(并且不是非常有用的)定义。出于实际目的,唯一有意义的定义是“内存泄漏是指程序在不再需要其保存的数据之后继续保存分配的内存的任何情况”。
梅森惠勒2014年

1
提到的acconrad的答案已删除?
托马什Zato -恢复莫妮卡

1
@TomášZato:不,不是。我现在将上面的参考链接了,因此您可以轻松找到它。
洋基

什么是物体复活?析构函数被调用多少次?这些问题如何反驳这个答案?
自闭症

1
好吧,尽管有GC,但请确保您可以在Java内部创建内存泄漏,并且仍然满足上述定义。只是具有仅附加的数据结构,可以防止外部访问,因此不会再有其他代码将其删除-该程序无法释放内存,因为它没有代码。
toolforger '18

39

这是通过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 here
        String 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));
...

为了增加缺点,您可能还.intern()需要子字符串:

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

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


4
我不认为这是一个内存泄漏,本身。当muchSmallerString释放时(因为StringLeaker对象被破坏),长字符串也将被释放。我所说的内存泄漏是在这种JVM实例中永远无法释放的内存。但是,您已经向自己展示了如何释放内存:this.muchSmallerString=new String(this.muchSmallerString)。发生真正的内存泄漏时,您无能为力。
RDS

2
@rds,这很公平。intern与“内存泄漏”相比,这种非案例可能更像是“内存意外”。.intern()但是,确定子字符串肯定会导致保留对较长字符串的引用,并且无法将其释放的情况。
乔恩·钱伯斯

15
substring()方法在java7中创建了一个新的String(这是一种新行为)
anstarovoyt 2013年

您甚至不需要自己做substring():使用一个正则表达式匹配器来匹配巨大输入的一小部分,并长时间携带“提取的”字符串。直到Java 6为止,大量输入仍然有效
。– Bananeweizen

37

GUI代码中的一个常见示例是在创建窗口小部件/组件并将侦听器添加到某个静态/应用程序作用域对象时,然后在销毁窗口小部件时不删除侦听器。不仅会发生内存泄漏,而且还会降低性能,因为无论您在听什么火灾事件,所有旧的监听器都将被调用。


1
android平台给出了通过在View的静态字段中缓存位图而造成的内存泄漏的示例。
rds

36

使用在任何servlet容器(Tomcat,Jetty,Glassfish等)中运行的任何Web应用程序。连续10或20次重新部署该应用程序(仅触摸服务器的自动部署目录中的WAR可能就足够了。

除非有人进行了实际测试,否则几经重新部署后,您很可能会收到OutOfMemoryError的错误,因为该应用程序在执行之后并未对其进行清理。通过此测试,您甚至可能在服务器中发现错误。

问题是,容器的寿命比应用程序的寿命更长。您必须确保可以对容器可能对应用程序的对象或类的所有引用进行垃圾收集。

如果只有一个引用幸免于Web应用程序的取消部署,则相应的类加载器以及结果将无法垃圾收集Web应用程序的所有类。

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


1
这不是因为内存泄漏,而是因为类加载器不会卸载先前的类集。因此,建议不要重新部署应用程序服务器而不重新启动服务器(不是物理机,而是应用程序服务器)。我已经看到了与WebSphere相同的问题。
斯文(Sven)'18年

35

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

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

但这是关于“标准”类型的内存泄漏,当您无法再访问该内存时,它仍归应用程序所有。您可以保留对未使用对象的引用,也可以在不关闭流的情况下打开流。


22
这取决于“内存泄漏”的定义。如果“保留但不再需要的内存”,那么在Java中很容易做到。如果是“已分配但根本无法通过代码访问的内存”,那么它将变得有点困难。
Joachim Sauer

@Joachim Sauer-我的意思是第二种。首先很容易做到:)
Rogach 2011年

6
“使用纯Java,几乎是不可能的。” 好吧,我的经验是另一回事,尤其是在那些不了解此处陷阱的人实施缓存的时候。
Fabian Barney

4
@Rogach:回答+10000的人对各种答案的投票基本上是+400,这表明在两种情况下Joachim Sauer都认为这很有可能。因此,您的“几乎不可能”毫无意义。
语法T3rr0r,2011年

32

与PermGen和XML解析有关的一次“内存泄漏”很好。我们使用的XML解析器(我不记得是哪个解析器)对标记名称执行了String.intern(),以使比较更快。我们的一位客户有个好主意,它不以XML属性或文本形式存储数据值,而是以标记名的形式存储数据,因此我们拥有一个文档,例如:

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

实际上,他们没有使用数字,而是使用更长的文本ID(大约20个字符),这些ID是唯一的,每天以10-15百万的速度出现。每天将产生200 MB的垃圾,不再需要,也不再进行GC(因为它在PermGen中)。我们将permgen设置为512 MB,因此内存不足异常(OOME)大约需要两天的时间到达...


4
只是挑剔您的示例代码:我认为数字(或以数字开头的字符串)不允许作为XML中的元素名称。
圣保罗Ebermann

请注意,这对于JDK 7+不再适用,因为JDK 7+会在堆上进行字符串实习。有关详细的文章,请参见本文:java-performance.info/string-intern-in-java-6-7-8
jmiserez

因此,我认为使用StringBuffer代替String可以解决此问题?不会吗
anubhs

24

什么是内存泄漏:

  • 这是由错误错误的设计。
  • 这是浪费内存。
  • 随着时间的流逝,情况变得越来越糟。
  • 垃圾收集器无法清理它。

典型示例:

缓存对象是弄乱事物的一个很好的起点。

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

您的缓存越来越大。很快,整个数据库就被吸入了内存。更好的设计使用LRUMap(仅将最近使用的对象保留在缓存中)。

当然,您可以使事情复杂得多:

  • 使用ThreadLocal构造。
  • 添加更复杂的参考树
  • 第三方图书馆造成的泄漏。

经常发生的情况:

如果此Info对象具有对其他对象的引用,则该对象又具有对其他对象的引用。从某种意义上讲,您还可以认为这是某种内存泄漏(由不良的设计引起)。


22

我认为没有人使用内部类示例很有趣。如果您有内部课程;它固有地维护对包含类的引用。当然,从技术上讲,这不是内存泄漏,因为Java最终会清除它;但这会导致课程停留时间比预期的更长。

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

现在,如果您调用Example1并获得一个Example2来丢弃Example1,则您仍然固有地具有指向Example1对象的链接。

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假定它将一直存在,并且如果再也无法在代码中实现,则实际上将永远不会尝试对其进行清理。但这是完全未经证实的。


2
内部类很少有问题。它们是一个简单的案例,非常容易检测。谣言也只是谣言。
bestsss 2011年

2
“谣言”听起来像是有人半信半疑地了解了世代GC的工作原理。寿命长但现在无法到达的对象确实可以留下来并占用一段时间,因为JVM将它们从年轻一代中提升出来,因此可以每过一遍就停止检查它们。他们会故意避开那些“清理我的5000个临时字符串”的过程。但是它们不是不朽的。他们仍然有资格进行收集,并且如果VM被限制用于RAM,它将最终运行完整的GC扫描并收回该内存。
cHao

22

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

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

为了存储特定于线程的标记,log4j的NDC类使用一个Hashtable,该哈希表由Thread对象本身作为键(而不是说线程ID),因此直到NDC标签保留在内存中的所有挂起线程的对象对象也保留在内存中。在我们的Web应用程序中,我们使用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对象。因此,即使在线程完成其正在执行的操作之后,NDC上下文Hastable仍保持对hugeList的引用,从而导致内存泄漏。


糟透了。您应该检查在记录文件时分配零内存的记录库:mentalog.soliveirajr.com
TraderJoeChicago 2011年

+1您是否立即知道slf4j / logback中的MDC是否存在类似问题(同一作者的后续产品)?我将深入探讨源代码,但想先检查一下。无论哪种方式,感谢您发布此信息。
sparc_spread 2014年

20

采访者可能正在寻找像下面的代码这样的循环引用(顺便说一句,这种引用只是在使用引用计数的非常老的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不再使用引用计数,大多数使用清除垃圾收集器,实际上它将收集此内存。

接下来,您可能会解释如何创建一个具有基础本机资源的对象,如下所示:

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感知的正常范围之外分配本地资源。


19

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

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

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


1
根据真实故事。
2015年

18

创建一个静态地图,并继续为其添加硬引用。这些将永远不会被GC。

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); }
}

87
那是怎么回事?它确实在执行您要执行的操作。如果那是泄漏,那么在任何地方创建和存储对象就是泄漏。
法尔马里

3
我同意@Falmarri。我在那里看不到泄漏,您只是在创建对象。您当然可以使用另一种称为“ removeFromCache”的方法来“回收”刚刚分配的内存。泄漏是指您无法回收内存。
凯尔2012年

3
我的观点是,如果有人不小心创建对象,或者将其放入缓存,可能会导致OOM错误。
duffymo

8
@duffymo:但这不是真正的问题。它与简单地耗尽所有内存无关。
法尔玛里2012年

3
绝对无效。您只是在Map集合中收集一堆对象。它们的参考将被保留,因为地图保留了它们。
gyorgyabraham 2013年

16

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

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 active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

我认为还没有人说过:您可以通过覆盖finalize()方法来使对象复活,以便finalize()在某个地方存储对此的引用。垃圾收集器将仅在对象上被调用一次,因此之后该对象将永远不会被破坏。


10
这是不正确的。finalize()将不会被调用,但是一旦没有更多引用,该对象将被收集。垃圾收集器也不被“调用”。
bestsss 2011年

1
这个答案是令人误解的,该finalize()方法只能由JVM调用一次,但这并不意味着如果重新使该对象然后再次取消引用该对象,则不能重新收集该方法。如果该finalize()方法中有资源关闭代码,则此代码将不会再次运行,这可能会导致内存泄漏。
汤姆·卡曼

15

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

等:您在当前工作目录中找到一个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.java
java -Xmx2m -classpath .:big.jar MemLeak

您不会在这里遇到OOM错误,因为不保留任何引用,因此无论您在上面的示例中选择ITERATIONS多大,应用程序都将继续运行。除非应用程序进入wait命令,否则进程(在顶部(RES / RSS)或进程浏览器中可见)的内存消耗会增加。在上面的设置中,它将分配大约150 MB的内存。

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

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

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

非常简单和令人惊讶。


14

正如很多人所建议的那样,资源泄漏很容易引起-就像JDBC示例一样。实际的内存泄漏要难一些-特别是如果您不依靠JVM的损坏位来为您做的话...

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

但是,曾经起作用的一种方法-我不知道它是否仍然起作用-是拥有三层深的圆形链条。正如对象A中对对象B的引用,对象B中对对象C的引用,对象C对对象A的引用。GC非常聪明,足以知道两条深链-如A <-> B -如果A和B无法通过其他任何方式访问,但无法处理三通链,则可以安全地收集它...


7
现在已经不是这种情况了。现代GC知道如何处理循环引用。
assylias

13

创造潜在的巨大的内存泄漏的另一种方式是坚持引用Map.Entry<K,V>TreeMap

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


现实场景:

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

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

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

您可以从该visualvm图中看到内存如何保持增长。

内存转储-TreeMap

散列数据结构(HashMap)不会发生相同的情况。

这是使用时的图形HashMap

内存转储-HashMap

解决方案?只需直接保存键/值(就像您可能已经做过的),而不是保存即可Map.Entry


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


11

直到终止线程才收集线程。它们是垃圾收集的根源。它们是为数不多的仅通过忘记它们或清除对它们的引用就不会回收的对象之一。

考虑:终止工作线程的基本模式是设置一些线程看到的条件变量。线程可以定期检查变量并将其用作终止信号。如果未声明变量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将永远不会消失。

(*编辑*)


1
@Spidey什么都没有“卡住”。调用方法会迅速返回,并且传递的对象将永远不会被回收。那恰恰是一个泄漏。
Boann 2013年

1
在程序的生命周期中,您将有一个线程“正在运行”(或处于休眠状态)。那不算是对我的泄漏。即使没有完全使用池,池也不算泄漏。
Spidey

1
@Spidey“您在程序的整个生命周期中都会拥有一个东西。这不算是对我的泄漏。” 你听到了吗?
Boann

3
@Spidey如果您要计数该进程知道没有泄漏的内存,那么这里的所有答案都是错误的,因为该进程始终跟踪其虚拟地址空间中映射了哪些页面。当进程终止时,操作系统通过将页面放回空闲页面堆栈中来清理所有泄漏。要将其推向另一个极端,可以指出任何RAM芯片中或磁盘上的交换空间中的物理位都没有被物理放错位置或破坏,从而将任何争执的泄漏弄死,因此您可以关闭计算机的电源。并再次清理所有泄漏。
Boann 2013年

1
泄漏的实际定义是丢失了我们不知道的内存,因此无法执行仅回收它的必要过程。我们将不得不拆除并重建整个内存空间。这样的流氓线程可以通过死锁或躲避线程池的实现自然而然地出现。现在,阻止了此类线程引用的对象(即使是间接引用的对象),因此,我们拥有的内存在程序生存期内将不会自然回收或重用。我认为这是一个问题;特别是内存泄漏。
Boann 2013年

10

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

例如,使用Servlet中的ThreadLocal变量与其他Web组件进行通信,使线程由容器创建并在池中维护空闲线程。如果未正确清除ThreadLocal变量,则该变量将一直存在,直到可能相同的Web组件覆盖它们的值为止。

当然,一旦确定,就可以轻松解决问题。


10

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

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

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

-韦斯·塔勒


12
这是引用计数垃圾收集器的经典问题。即使在15年前,Java也不使用引用计数。参考 计数也比GC慢。
bestsss 2011年

4
没有内存泄漏。只是一个无限循环。
Esben Skov Pedersen

2
@Esben在每次迭代中,前一个first都没有用,应该进行垃圾回收。在引用计数垃圾收集器中,对象不会被释放,因为它上面有一个活动的引用(本身)。无限循环是在这里演示泄漏:运行程序时,内存将无限期增加。
rds

@rds @ Wesley Tarle认为循环不是无限的。还会有内存泄漏吗?
nz_18年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.