使用Java创建内存泄漏

Mat*_* B. 3076 java memory memory-leaks

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

一个例子是什么?

Dan*_*den 2225

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

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

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

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

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

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

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

Pra*_*ate 1177

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

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}
Run Code Online (Sandbox Code Playgroud)

调用String.intern()冗长的String

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();
Run Code Online (Sandbox Code Playgroud)

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

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}
Run Code Online (Sandbox Code Playgroud)

未封闭的连接

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}
Run Code Online (Sandbox Code Playgroud)

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

在Web应用程序中,某些对象存储在应用程序范围中,直到明确停止或删除应用程序.

getServletContext().setAttribute("SOME_MAP", map);
Run Code Online (Sandbox Code Playgroud)

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

请参阅IBM jdk设置.

  • 我不同意上下文和会话属性是"泄漏".它们只是长期存在的变量.静态最终字段或多或少只是一个常数.也许应避免使用大常量,但我认为将其称为内存泄漏是不公平的. (171认同)
  • *(未封闭)开放流(文件,网络等...)*,在完成期间(将在下一个GC循环之后)不会泄漏真实的泄漏(close)将被安排(`close()`通常不会在终结器线程中调用,因为可能是阻塞操作).不关闭是一种不好的做法,但它不会导致泄漏.未公开的java.sql.Connection是相同的. (74认同)
  • 静态字段持有对象引用[esp final field]如何是内存泄漏? (40认同)
  • 在大多数理智的JVM中,似乎String类对其`intern`哈希表内容只有弱引用.因此,它*被正确地收集垃圾而不是泄漏.(但IANAJP)http://mindprod.com/jgloss/interned.html#GC (29认同)
  • @cHao True.我遇到的危险不是Streams泄露内存的问题.问题是没有足够的内存泄漏.你可以泄漏很多手柄,但仍然有足够的内存.然后,垃圾收集器可能决定不打算进行完整的收集,因为它仍然有足够的内存.这意味着不会调用终结器,因此您的句柄用完了.问题是,终结器(通常)会在泄漏流中耗尽内存之前运行,但在用完内存以外的其他内容之前可能不会调用它. (5认同)
  • 参考循环过去也是泄漏.这就是你有实例A引用实例B的地方,它引用实例C,引用实例A,但没有其他任何引用它们的实例.(循环可能需要更大) (2认同)
  • `finalize()`将被调用打开文件,套接字等.它将调用`close()`.[FileInputStream的文档](http://docs.oracle.com/javase/6/docs/api/java/io/FileInputStream.html#finalize%28%29)实际上特别说了.但这里的文档几乎无关紧要.终结器的全部原因是清理本来会泄漏的东西,任何处理本机资源但没有实现`finalize()`的类都会严重破坏.幸运的是,`java.net`和`java.io`是由了解Java应该如何工作的人编写的.:) (2认同)
  • 垃圾收集语言中的内存泄漏(更恰当地称为无意的对象保留)是隐蔽的.如果无意中保留了对象引用,则不仅将该对象从垃圾回收中排除,而且该对象引用的任何对象也是如此,依此类推.即使无意中保留了少量对象引用,也可以防止许多对象被垃圾收集,对性能可能产生很大影响. (2认同)
  • @cHao当然.我认为我看到的示例是在某种AWS资源流的大约65k个不同句柄处.我同意你需要疯狂的金额.我只是想指出在RAM耗尽之前你可能会用尽"句柄"或其他任何东西.或者,在托管堆上有足够空间的情况下,可能会耗尽内存,因为GC没有看到对象从堆中创建的实际内存压力. (2认同)

Pet*_*rey 448

一件简单的事情做的是使用与不正确的(或不存在)一个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.
Run Code Online (Sandbox Code Playgroud)

  • 我同意,这不是**内存"泄漏",因为你可以删除对hashset的引用并等待GC启动,并且presto!记忆又回来了. (93认同)
  • 实际上,即使元素类获取hashCode并且等于错误,您也可以从HashSet中删除元素; 只需获取集合的迭代器并使用其remove方法,因为迭代器实际上对底层条目本身而不是元​​素进行操作.(注意,未实现的hashCode/equals足以触发泄漏;默认值实现简单的对象标识,因此您可以获取元素并正常删除它们.) (66认同)
  • @Donal我想说的是,我猜,我不同意你对内存泄漏的定义.我会考虑(继续比喻)你的迭代器去除技术是泄漏下的滴水盘; 无论滴盘如何,泄漏仍然存在. (11认同)
  • @ SyntaxT3rr0r,我将你的问题解释为询问语言中是否有任何自然导致内存泄漏的内容.答案是不.这个问题询问是否有可能设法创建类似内存泄漏的情况.这些示例都不是C/C++程序员理解它的方式的内存泄漏. (11认同)
  • @Peter Lawrey:另外,你怎么看待这个:*"如果你不忘记手动释放你分配的内存,那么C语言中没有任何内容会自然泄漏到内存泄漏"*.这对智力不诚实有什么影响?无论如何,我很累:你可以说最后一句话. (10认同)
  • @ SyntaxT3rr0r:你所有的帖子似乎都围绕着"狂热主义"和"公然谎言"以及"知识分子不诚实"的指责......如果你想讨厌Java我肯定会有一些远离这里你可以生气的论坛在不污染技术对话的情况下更容易咆哮. (8认同)
  • @ SyntaxT3rr0r,经过反思,我认为你有一个很好的观点,但你要感受到情感而不是技术上的争论. (7认同)
  • @ SyntaxT3rr0r,我已经写了一个更详细的回答你有趣的问题http://vanillajava.blogspot.com/2011/07/java-and-memory-leaks.html (5认同)
  • @Donal你最后的陈述是不正确的.您无法获取元素,因为您没有引用它.对地图中放置的键的唯一引用是地图本身所持有的那个.只有你去了'BadKey myKey = new BadKey("key"); map.put(key,"value");`你能不能把它拿出来.你是正确的,你可以使用迭代器来删除它,但你不能总是对所有数据结构这样做.例如,如果你没有在@meriton的答案中取消引用,那将永远丢失.您无法访问支持数组,迭代器将停止它. (2认同)

bes*_*sss 266

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

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

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

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

  • 创建(但不是开始)a Thread与上面的类别相同.

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

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

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

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

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

  • 使用InflaterInputStream传入new java.util.zip.Inflater()构造函数(PNGImageDecoder例如)而不调用end()inflater.好吧,如果你只用构造传递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.最后两个中最好的部分:您无法通过常规的分析工具找到它们.

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

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

  • "创造但不是开始一个线程......"哎呀,几个世纪前我被这个人严重咬了!(Java 1.3) (23认同)
  • @bestsss :我很好奇,鉴于它在 JVM 关闭时运行,为什么要删除关闭挂钩? (2认同)

Nic*_*uet 196

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

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

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

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

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

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

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

  • 我觉得很有趣的是,在其他答案中,人们正在寻找那些边缘案例和技巧,似乎完全忽略了这一点.他们可以只显示对无法再次使用的对象进行无用引用的代码,并且永远不会删除这些引用; 可能会说这些情况不是"真正的"内存泄漏,因为仍有对这些对象的引用,但如果程序从不再使用这些引用并且也从不丢弃它们,那么它完全等同于(并且一样糟糕)"真正的内存泄漏". (22认同)

Pla*_*ank 155

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

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

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

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

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

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

  • 每当我提出这个问题时,我都在寻找一个非常简单的答案 - 继续增长队列,没有最终关闭数据库等,而不是奇怪的类加载器/线程细节,暗示他们理解gc可以做什么,不能为你做什么.取决于你正在面试的工作我猜. (21认同)

Vin*_*lds 128

如果您不了解JDBC,以下是一个非常毫无意义的示例.或者至少JDBC期望如何开发人员关闭Connection,StatementResultSet实例丢弃它们或失去对它们的引用,而不是依赖于实施前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);
   }
}
Run Code Online (Sandbox Code Playgroud)

上面的问题是Connection对象没有关闭,因此物理连接将保持打开状态,直到垃圾收集器出现并看到它无法访问.GC将调用该finalize方法,但是有些JDBC驱动程序没有实现finalize,至少与Connection.close实现的方式不同.由此产生的行为是,由于收集了无法访问的对象而将回收内存,因此Connection可能无法回收与该对象关联的资源(包括内存).

在这种情况下,其中Connectionfinalize方法不清理一切,一个可能真正发现到数据库服务器的物理连接将持续几个垃圾回收周期,直到该数据库服务器最终计算出该连接是不是还活着(如果它并且应该关闭.

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

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

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

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

  • 在达到内存限制之前,您将达到最大打开连接限制.不要问我为什么知道...... (96认同)

mer*_*ike 118

可能是潜在内存泄漏的最简单示例之一,以及如何避免它,是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;
}
Run Code Online (Sandbox Code Playgroud)

如果你自己实现它,你会想到清除不再使用的数组元素(elementData[--size] = null)吗?那个引用可能会让一个巨大的物体活着......

  • @maniek:我并不是故意暗示这段代码会出现内存泄漏.我引用它来表明有时需要非显而易见的代码来避免意外的对象保留. (27认同)
  • Joshua Bloch在Effective Java中给出了这个示例,展示了Stacks的简单实现.一个非常好的答案. (6认同)
  • 那里的内存泄漏在哪里? (5认同)

Bil*_*ard 65

每当你保持对不再需要的对象的引用时,就会发生内存泄漏.有关内存泄漏如何在Java中表现出来以及您可以采取哪些措施的示例,请参阅Java程序中的处理内存泄漏.

  • @Mehrdad:这只是一个不完全适用于所有语言的狭义定义.我认为*任何*内存泄漏都是由程序设计不佳引起的错误. (28认同)
  • 我不相信这是"泄密".这是一个错误,它是由程序和语言设计的.泄漏将是一个悬挂在*周围的物体,而没有任何对它的引用. (14认同)
  • @Mehrdad:`...那么问题是"你如何在X中创建内存泄漏?" 变得毫无意义,因为它可以用任何语言表达.我不知道你是如何得出这个结论的.任何定义都有*更少*在Java中创建内存泄漏的方法.这绝对是一个有效的问题. (9认同)
  • @ 31eee384:如果你知道事实不会,那就不可能了.所写的程序永远不会访问数据. (8认同)
  • @ 31eee384:如果你的程序将对象保存在内存中,它永远不能使用,那么从技术上来说它就是泄漏的内存.你有更大问题的事实并没有真正改变这一点. (6认同)
  • 我明白你的意思(并同意其中的一些内容),但问题是,如果以这种方式看待它,那么问题是"你如何在X中创建内存泄漏?" 变得毫无意义,因为它可以用*任何*语言.虽然它可能是真的,但它并不是特定于Java,所以我不认为这就是我们所说的"漏洞". (2认同)
  • @ 31eee384:如果你保持对一个对象的引用并且从不删除它,那么在JVM被杀之前也不会检索内存.有些人不会称之为"真正的内存泄漏",因为某处有引用; 但是如果程序永远不会再次使用该对象,同时该程序也永远不会删除该引用,那么它与"真正的内存泄漏"一样重要,人们在这里寻找一个有趣的练习(或者更重要的是,因为它们比人们为了得到"真正的"内存泄漏而在他们的答案上创建的边缘情况更常见) (2认同)

ste*_*emm 49

您可以使用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();
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 分配的内存也不属于Java. (4认同)
  • 这将构建在eclipse中(至少在最近的版本中),但是您需要更改编译器设置:在Window> Preferences> Java> Compiler> Errors/Warning>已过时且受限制的API集Forbidden reference(访问规则)为"Warning" ". (3认同)
  • 内存当然确实"属于Java",至少在某种意义上说i)它对任何人都不可用,ii)当Java应用程序退出时它将返回到系统.它就在JVM之外. (2认同)

yan*_*kee 43

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

"当计算机程序消耗内存但无法将其释放回操作系统时,会发生计算机科学(或泄漏,在此上下文中)的内存泄漏." (维基百科)

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

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

答案很长:您可以通过使用JNI编写Java库来获取内存泄漏,JNI可能具有手动内存管理功能,因此存在内存泄漏.如果你调用这个库,你的java进程将泄漏内存.或者,您可能在JVM中有错误,因此JVM会丢失内存.JVM中可能存在bug,甚至可能有一些已知的bug,因为垃圾收集不是那么简单,但它仍然是一个bug.按设计这是不可能的.你可能会要求一些受这种bug影响的java代码.对不起,我不知道一个,无论如何它在下一个Java版本中可能不再是一个bug.

  • 这是对内存泄漏的极其有限(并且不是非常有用)的定义.对于实际目的而言唯一有意义的定义是"内存泄漏是程序在不再需要的数据之后继续保持分配内存的任何条件." (10认同)

Jon*_*ers 37

这是一个简单/险恶的,来自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);
    }
}
Run Code Online (Sandbox Code Playgroud)

因为子串指的是原始字符串的内部表示,所以原始字符串保留在内存中.因此,只要你有一个StringLeaker,你就可以在内存中拥有整个原始字符串,即使你可能认为你只是坚持使用单字符字符串.

避免存储对原始字符串的不需要的引用的方法是执行以下操作:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...
Run Code Online (Sandbox Code Playgroud)

对于增加的不良,您可能还有.intern()子字符串:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...
Run Code Online (Sandbox Code Playgroud)

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

  • 方法substring()在java7中创建一个新的String(这是一个新行为) (14认同)
  • 我不会称之为内存泄漏,_per se_.当`muchSmallerString`被释放时(因为`StringLeaker`对象被销毁),长字符串也将被释放.我称之为内存泄漏的内存在此JVM实例中永远不会被释放.但是,你已经证明了自己如何释放内存:`this.muchSmallerString = new String(this.muchSmallerString)`.有了真正的内存泄漏,你无能为力. (4认同)
  • @rds,这是一个公平的观点.非"内部"案例可能更像是"内存泄露",而不是"内存泄漏".但是,`.intern()`子字符串肯定会产生这样一种情况:对较长字符串的引用被保留并且不能被释放. (2认同)

Har*_*ann 36

获取在任何servlet容器中运行的任何Web应用程序(Tomcat,Jetty,Glassfish,等等......).连续重新部署应用程序10次或者20次(可能只需触摸服务器的autodeploy目录中的WAR即可.

除非有人实际对此进行了测试,否则在进行几次重新部署后你会得到一个OutOfMemoryError的可能性很高,因为应用程序没有注意自己清理.您甚至可以通过此测试在服务器中找到错误.

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

如果只有一个引用在您的Web应用程序取消部署后仍然存在,则相应的类加载器将导致您的Web应用程序的所有类都无法进行垃圾回收.

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

  • 这不是因为内存泄漏,而是因为类加载器没有卸载之前的一组类。因此,不建议在不重启服务器(不是物理机,而是应用服务器)的情况下重新部署应用服务器。我在 WebSphere 上也看到过同样的问题。 (2认同)

pil*_*rth 35

GUI代码中的一个常见示例是创建窗口小部件/组件并将侦听器添加到某个静态/应用程序范围对象,然后在窗口小部件被销毁时不删除侦听器.您不仅会遇到内存泄漏,而且还会受到性能影响,因为无论您何时正在收听点火事件,您的所有旧听众都会被调用.


Rog*_*ach 33

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

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

但这是关于"标准"类型的内存泄漏,当您无法再访问内存时,它仍然由应用程序拥有.您可以保留对未使用对象的引用,或者在不关闭它们的情况下打开流.

  • 这取决于"内存泄漏"的定义.如果"内存仍然存在,但不再需要",那么在Java中很容易做到.如果它是"已经分配但根本无法通过代码访问的内存",那么它会变得稍微困难​​一些. (21认同)
  • "使用纯Java,几乎是不可能的." 好吧,我的经验是另一个特别是当涉及到那些不了解这里的陷阱的人实施缓存时. (6认同)
  • @Rogach:对于那些有+10 000名代表的人来说,各种答案基本上都有+400票,这两个案例都表明*Joachim Sauer*评论说这很有可能.所以你的"几乎不可能"毫无意义. (4认同)

Ron*_*Ron 29

我曾经有过一次与PermGen和XML解析有关的"内存泄漏".我们使用的XML解析器(我不记得它是哪一个)在标记名称上做了一个String.intern(),以便更快地进行比较.我们的一位客户最好不要将数据值存储在XML属性或文本中,而是存储为标记名,因此我们有一个文档,如:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>
Run Code Online (Sandbox Code Playgroud)

实际上,他们并没有使用数字,而是使用更长的文本ID(大约20个字符),这些ID是独一无二的,并且以每天1000-1500万的速度进行.这每天产生200 MB的垃圾,这是永远不需要的,而且从来没有GCed(因为它是在PermGen).我们将permgen设置为512 MB,因此内存异常(OOME)需要大约两天才能到达......

  • 只是为了挑选你的示例代码:我认为数字(或以数字开头的字符串)不允许作为XML中的元素名称. (4认同)

bvd*_*vdb 23

什么是内存泄漏:

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

典型例子:

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

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;
}
Run Code Online (Sandbox Code Playgroud)

您的缓存增长和增长.很快整个数据库就被吸进了内存.更好的设计使用LRUMap(仅在缓存中保留最近使用的对象).

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

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

经常发生的事情:

如果此Info对象具有对其他对象的引用,则其他对象也会引用其他对象.在某种程度上,你也可以认为这是某种内存泄漏(由糟糕的设计引起).


Sur*_*oot 22

我觉得有趣的是没有人使用内部类的例子.如果你有内部课程; 它固有地维护对包含类的引用.当然,从技术上讲,它不是内存泄漏,因为Java最终会将其清理干净; 但这可能导致课程比预期更长时间.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果您调用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.
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Pun*_*eet 22

我最近遇到了log4j造成的内存泄漏情况.

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

为了存储特定于线程的标记,log4j的NDC类使用一个由Thread对象本身键入的Hashtable(而不是线程id),因此直到NDC标记在内存中保留所有挂起线程的对象对象也留在记忆中.在我们的Web应用程序中,我们使用NDC标记带有请求ID的logoutput,以区分日志和单个请求.将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();
    }
}    
Run Code Online (Sandbox Code Playgroud)

因此,NDC上下文与生成的内联线程相关联.作为此NDC上下文的键的线程对象是内联线程,其中挂起了hugeList对象.因此,即使在线程完成它正在做的事情之后,对NDC上下文Hastable仍然保持对hugeList的引用,从而导致内存泄漏.


del*_*106 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 */
}
Run Code Online (Sandbox Code Playgroud)

然后你可以用引用计数来解释,上面的代码会泄漏内存.但是大多数现代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. */
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在一天结束时,使用现代JVM,您需要编写一些Java代码,以便在JVM意识的正常范围之外分配本机资源.

  • 没有 JVM 使用过引用计数。您可能会对 Microsoft 早期实现的 Java**script** 感到困惑。 (2认同)

duf*_*ymo 18

创建一个静态Map并继续添加对它的硬引用.那些永远不会是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); }
}
Run Code Online (Sandbox Code Playgroud)

  • 怎么泄漏?它正在做你要求它做的事情.如果这是泄漏,在任何地方创建和存储对象都是泄漏. (84认同)
  • @duffymo:但问题并非如此.它与简单地耗尽所有记忆无关. (8认同)
  • 反对这个答案的评论非常有趣,因为超过 2000 票的公认答案说的是完全相同的事情,尽管是以一种高度混淆的方式。ThreadLocals 并不神奇——它们只是 Map 中的条目,由 Thread 中的成员变量指向。 (5认同)
  • 我同意@Falmarri.我没有看到泄漏,你只是在创建对象.您当然可以"回收"您刚刚使用另一个名为"removeFromCache"的方法分配的内存.当您无法回收内存时,泄漏就会发生. (3认同)
  • 我的观点是,持续创建对象的人,可能将它们放入缓存中,如果不小心,最终可能会出现OOM错误. (3认同)
  • 绝对无效.您只是在Map集合中收集了一堆对象.他们的参考文献将保留,因为地图持有它们. (3认同)
  • 这是 Java 中经典的内存泄漏。这不是 JVM 失去对内存的跟踪的意义上的泄漏,而是 JVM 永远不会对其进行垃圾收集的意义上的泄漏。如果您监视使用此构造的应用程序的堆,它将具有普遍增加的趋势并最终以 OutOfMemoryError 终止。这就是内存泄漏的定义。 (3认同)

Pau*_*rie 17

每个人总是忘记原生代码路由.这是泄漏的简单公式:

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

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


set*_*ien 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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Ben*_*Ben 15

我认为还没有人说过这个:你可以通过覆盖finalize()方法来复活一个对象,以便finalize()在某处存储这个引用.垃圾收集器只会在对象上调用一次,所以在此之后对象永远不会被销毁.

  • 这是不真实的.`finalize()`不会被调用,但是一旦没有更多的引用就会收集对象.垃圾收集器也不会被"调用". (10认同)

Jay*_*Jay 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

只需粘贴到名为BigJarCreator.java的文件中,从命令行编译并运行它:

javac BigJarCreator.java
java -cp . BigJarCreator
Run Code Online (Sandbox Code Playgroud)

Etvoilà:你在当前的工作目录中找到一个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);
    }

}
Run Code Online (Sandbox Code Playgroud)

这个类基本上什么都不做,但创建了未引用的InputStream对象.这些对象将立即被垃圾收集,因此不会对堆大小有所贡献.对于我们的示例来说,从jar文件加载现有资源非常重要,大小在这里很重要!

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

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
Run Code Online (Sandbox Code Playgroud)

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

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

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
Run Code Online (Sandbox Code Playgroud)

并且您的过程不会超过35 MB,与迭代次数无关.

非常简单和令人惊讶.


Gra*_*ham 14

正如许多人所建议的那样,资源泄漏很容易引起 - 就像JDBC示例一样.实际内存泄漏有点困难 - 特别是如果你不依赖于JVM的破碎位来为你做这件事......

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

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

  • 一段时间以来一直没有这样的情况.现代GC知道如何处理循环引用. (7认同)

Ben*_* Xu 11

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

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 expose 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 {

}
Run Code Online (Sandbox Code Playgroud)


Boa*_*ann 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();
}
Run Code Online (Sandbox Code Playgroud)

打电话给System.gc()你喜欢,但传递给的对象leakMe永远不会死.

(*编辑*)

  • @Spidey如果你计算进程知道没有泄露的内存,那么这里的所有答案都是错误的,因为进程总是跟踪其虚拟地址空间中的哪些页面被映射.当进程终止时,操作系统会通过将页面放回到空闲页面堆栈来清除所有泄漏.为了把它带到下一个极端,通过指出RAM芯片中或磁盘上的交换空间中的任何物理位都没有被物理错位或破坏,可以击败任何有争议的泄漏,因此您可以关闭计算机并再次清理任何泄漏. (3认同)

msc*_*ker 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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

这是引用计数垃圾收集器的典型问题.然后,您可以礼貌地解释JVM使用更复杂的算法,但没有此限制.

-Wes Tarle

  • *这是引用计数垃圾收集器的典型问题.*即使15年前,Java也没有使用引用计数.参考.计数也比GC慢. (11认同)
  • 不是内存泄漏.只是一个无限循环. (3认同)
  • @Esben在每次迭代时,先前的`first`都没用,应该是垃圾回收.在_reference counting_垃圾收集器中,对象不会被释放,因为它上面有一个活动引用(单独).无限循环在这里是为了说明泄漏:当你运行程序时,内存将无限期地提升. (2认同)

Mar*_*cak 10

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

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


现实场景:

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

public static Map<String, Integer> pseudoQueryDatabase();
Run Code Online (Sandbox Code Playgroud)

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

考虑以下包装类:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}
Run Code Online (Sandbox Code Playgroud)

应用:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

在每次pseudoQueryDatabase()调用之后,map实例应该准备好进行收集,但是不会发生,因为至少有一个Entry存储在其他地方.

根据您的jvm设置,应用程序可能会因为a而在早期阶段崩溃OutOfMemoryError.

您可以从此visualvm图中看到内存如何不断增长.

内存转储 -  TreeMap

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

这是使用a时的图表HashMap.

内存转储 -  HashMap

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


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


小智 9

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

GC javadoc片段:

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

图片javadoc片段:

应用程序代码必须显式调用Image.dispose()方法,以便在不再需要这些实例时释放由每个实例管理的操作系统资源.


小智 8

理论上你不能.Java内存模型可以防止它.但是,由于必须实现Java,因此您可以使用一些注意事项.取决于你可以使用什么:

  • 如果你可以使用native,你可以分配你以后不会放弃的内存.

  • 如果没有,那么关于java的一个肮脏的小秘密并没有人知道.您可以要求不由GC管理的直接访问阵列,因此可以很容易地用于使内存泄漏.这是由DirectByteBuffer(http://download.oracle.com/javase/1.5.0/docs/api/java/nio/ByteBuffer.html#allocateDirect(int))提供的.

  • 如果你不能使用其中任何一个,你仍然可以通过欺骗GC来造成内存泄漏.JVM使用Generational垃圾收集实现.这意味着堆被分为几个区域:年轻人,成年人和老年人.创建的对象从年轻区域开始.随着他的使用越来越多,他逐渐成长为成年人.到达老年人区域的物体很可能不会被收集.您不能确定物体是否泄漏,如果您要求停止并清洁GC,它可能会清洁它,但是很长一段时间它会被泄漏.更多信息,请访问(http://java.sun.com/docs/hotspot/gc1.4.2/faq.html)

  • 此外,类对象不需要GC.我可以这样做.

  • 当DirectByteBuffer被垃圾收集时,DirectByteBuffer分配的本机内存在终结器中被释放.它当然不泄漏. (2认同)

h22*_*h22 8

一个不终止的线程(比如在其run方法中无限期地休眠).即使我们放弃了对它的引用,它也不会被垃圾收集.您可以添加字段以使线程对象变得很大.

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


Pav*_*nov 8

我想就如何使用JVM中提供的工具监视内存泄漏的应用程序提出建议.它没有说明如何生成内存泄漏,但解释了如何使用最少的工具检测它.

您需要首先监视Java内存消耗.

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

jstat -gcutil <process_id> <timeout>
Run Code Online (Sandbox Code Playgroud)

它将报告每一代(Young,Eldery和Old)的内存消耗和垃圾收集时间(Young和Full).

一旦发现Full Garbage Collection执行得太频繁并且花费太多时间,您就可以假设应用程序正在泄漏内存.

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

jmap -dump:live,format=b,file=heap.bin <process_id>
Run Code Online (Sandbox Code Playgroud)

然后,您需要使用Memory Analyzer,Eclipse Memory Analyzer(MAT)分析heap.bin文件.

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


arn*_*rnt 7

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

进程A通过TCP与B通信,并告诉进程B创建一些东西.B向资源发出一个ID,比如说432423,A存储在一个对象中并在与B交谈时使用.在某些时候,A中的对象被垃圾收集回收(可能是由于一个bug),但A从不告诉B(也许是另一个bug).

现在A不再具有它在B的RAM中创建的对象的ID,并且B不知道A没有对该对象的更多引用.实际上,该对象被泄露.


Pet*_*ter 7

一些建议:

  • 在servlet容器中使用commons-logging(可能有点挑衅)
  • 在servlet容器中启动一个线程,不要从它的run方法返回
  • 在servlet容器中加载GIF动画(这将启动一个动画线程)

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

最近偶然发现了这个:

  • 调用"new java.util.zip.Inflater();" 没有调用"Inflater.end()"

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


abi*_*rai 7

如果最大堆大小是X. Y1 .... Yn没有实例所以,总内存=实例数X每个实例的字节数.如果X1 ...... Xn是每个实例的字节数.那么总内存(M)= Y1*X1 + ..... + Yn*Xn.所以,如果M> X它超过了堆空间.以下可以是代码1中的问题.使用更多实例变量然后是本地变量.2.每次创建实例而不是汇集对象.3.不按需创建对象.4.在操作完成后使对象引用为null.再次,在程序中需要时重新创建.


Bas*_*Roy 7

从finalize方法中抛出未处理的异常.


Ale*_*tos 7

关于如何在Java中创建内存泄漏有很多答案,但请注意在访谈期间提出的要点.

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

如果我问你"你有解决Java中内存泄漏问题的经验吗?",你的回答将是一个简单的"是".然后我会跟进"你能举例说明你在哪里解决内存泄漏问题吗?",你会给我一两个例子.

然而,当面试官问"如何用Java创建内存泄漏?" 预期答案应遵循以下方针:

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

当开发人员不遵循这一思路时,我会尝试引导他/她问"你能给我一个Java泄漏内存的例子吗?",然后是"你有没有必要修复Java中的任何内存泄漏?"

请注意,我不是要求一个关于如何在Java中泄漏内存的示例.那太傻了.谁会对能够有效编写泄漏内存的代码的开发人员感兴趣?


Gra*_*ity 6

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


小智 6

java中的内存泄漏不是典型的C/C++内存泄漏.

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

基本上,重要的是:

标记和扫描模型

JRockit JVM使用标记和清除垃圾收集模型来执行整个堆的垃圾收集.标记和扫描垃圾收集包括两个阶段,标记阶段和扫描阶段.

在标记阶段,可以从Java线程,本机句柄和其他根源可访问的所有对象都标记为活动,以及可从这些对象访问的对象,等等.此过程标识并标记仍在使用的所有对象,其余对象可视为垃圾.

在扫描阶段,遍历堆以找到活动对象之间的间隙.这些间隙记录在空闲列表中,可用于新对象分配.

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

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

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

值得注意的是,所有GC语言都适用.下面,我看到一些作为SRE工作的例子:

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


Cha*_*ase 5

在Java中,"内存泄漏"主要是你使用了太多的内存,这与你不再使用内存而忘记返回(免费)的C不同.当面试官询问Java内存泄漏时,他们会询问JVM内存使用情况是否会继续上升,并且他们确定定期重新启动JVM是最佳解决方案.(除非是面试官是非常精通技术)

所以回答这个问题,就好像他们询问是什么让JVM内存使用量随着时间而增长.好的答案是在HttpSessions中存储过多的数据,超时时间过长或内存缓存(Singleton)实现不好,从而不会刷新旧条目.另一个可能的答案是拥有大量JSP或动态生成的类.类被加载到一个名为PermGen的内存区域,该区域通常很小,大多数JVM都不实现类卸载.


jor*_*geu 5

使用对话框,Swing非常容易.创建一个JDialog,显示它,用户关闭它,泄漏!你必须打电话dispose()或配置setDefaultCloseOperation(DISPOSE_ON_CLOSE)


Vir*_*raj 5

java 1.6中的String.substring方法创建内存泄漏.这篇博文解释了它.

http://javarevisited.blogspot.com/2011/10/how-substring-in-java-works.html


Tas*_*isi 5

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

示例 1:

这是Effective 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是书中描述为什么这种实现会导致内存泄漏的段落:

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

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

解决这类问题的方法很简单:一旦引用过时,就将其归零。在我们的 Stack 类的情况下,一旦它从堆栈中弹出,对项目的引用就变得过时了。pop 方法的修正版本如下所示:

public Object pop() {
    if (size == 0) throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}
Run Code Online (Sandbox Code Playgroud)

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

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

示例 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

public void removeObserver(Observer observer) {
    observers.remove(observer);
}
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

615453 次

最近记录:

5 年,11 月 前