为什么有些网络服务器会抱怨他们创建的内存泄漏?

标题可能有点强,但让我解释一下我如何理解会发生什么。 我猜这发生在Tomcat上(引用的消息来自Tomcat),但我不确定了。

TL; DR在底部有一个总结,为什么我声称它是Web服务器的错。

我可能错了(但没有错误的可能性,没有理由问):

  • 应用程序使用库
  • 该库使用ThreadLocal
  • ThreadLocal引用库中的对象
  • 每个对象引用其ClassLoader

网络服务器

  • 汇集其工作线程以提高效率
  • 为应用程序提供任意线程
  • 当应用程序停止或重新部署时,没有什么特别的(在线程池中)

如果我理解正确,在重新部署后,旧的“脏”线程继续被重用。 他们的ThreadLocal引用引用其ClassLoader的旧类,它引用整个旧类层次结构。 所以很多东西都停留在PermGen空间中,随着时间的推移会导致OutOfMemoryError到目前为止这是对的吗?


我假设有两件事:

  • 重新部署频率是每小时几次
  • 线程创建开销只是毫秒的一小部分

因此,每次重新部署时完整的线程池更新每小时花费几分之一毫秒,即,时间开销为0.0001 * 12/3600 * 100%0.000033%

但是,不是接受这个微小的开销,而是有无数的问题 。 我的计算错了还是我忽略了什么?


作为警告,我们得到了消息

Web应用程序…使用类型为…的键创建了一个ThreadLocal,并且值为…但在Web应用程序停止时无法将其删除。

应该更好地说明

Web服务器…使用线程池但在停止(或重新部署)应用程序后无法续订。

或者我错了? 即使所有线程不时重新创建,时间开销也可以忽略不计。 但是在将ThreadLocal提供给应用程序之前清除它们就足够了,甚至更快。

概要

有一些真正的问题(最近这个 ),用户无能为力。 图书馆作家有时可以而且有时不可以。 恕我直言,网络服务器可以很容易地解决它。 事情发生了并且有原因。 所以我责怪唯一一个可以对此采取任何行动的政党。

关于Web服务器应该做什么的建议

这个问题的标题比正确的更具挑衅性,但它有其重要意义。 raphw的答案也是如此。 这个相关问题有另一个公开的赏金。

我认为Web服务器可以解决它如下:

  • 确保每个线程在某个时候被重用(或杀死)
  • ThreadLocal存储LastCleanupTimestamp (对于新线程,它是创建时间)
  • 当重新使用线程时,检查清理时间戳是否低于某个阈值(例如,现在减去一些delta ,例如1小时)
  • 如果是这样,请清除所有ThreadLocal并设置一个新的LastCleanupTimestamp

这将确保没有这样的泄漏存在长于delta加上最长请求的持续时间加上线程周转时间。 费用如下:

  • 每个请求检查一个ThreadLocal (即一些纳秒)
  • reflection清理所有ThreadLocal (即,每个线程每个delta一次更多纳秒)
  • 删除可能对存储它们的应用程序有用的数据的成本。 这不会破坏应用程序,因为没有应用程序可以假设看到包含它已设置的线程本地的线程(因为它甚至不能假设再看到线程本身),但它可能需要花费时间来重新创建数据(例如,一个缓存的DateFormat实例,如果有人仍然使用这样一个可怕的东西)。

如果最近没有取消部署或重新部署应用程序,可以通过简单地设置thresold来关闭它。

TL; DR它不是创建内存泄漏的Web服务器。 是你。

让我首先更明确地陈述问题: ThreadLocal变量通常是指由ClassLoader加载的Class实例,该ClassLoader是由容器的应用程序专门使用的。 当取消部署此应用程序时, ThreadLocal引用将变为孤立状态。 由于每个实例都保留对其Class的引用,并且由于每个Class保留对其ClassLoader的引用,并且由于每个ClassLoader保留对它所加载的所有类的引用,因此取消部署的应用程序的整个类树无法获取垃圾并且JVM实例会受到影响内存泄漏 。

看看这个问题,你可以优化:

  • 即使在整个重新部署期间也允许每秒尽可能多的请求(从而保持响应时间短并重用线程池中的线程)
  • 在重新部署时使用线程,确保线程保持清洁 (因此修补程序忘记了手动清理)

Web应用程序的大多数开发人员都认为第一个更重要,因为第二个可以通过编写好的代码来实现。 当重新部署与长期请求同时发生时会发生什么? 您无法关闭线程池,因为这会中断正在运行的请求。 (请求周期可以花费多长时间没有全局定义的最大值。)最后,您需要一个非常复杂的协议,这会带来自己的问题。

但是,可以通过始终写入来避免ThreadLocal引发的泄漏:

 myThreadLocal.set( ... ); try { // Do something here. } finally { myThreadLocal.remove(); } 

这样,您的线程将始终变得干净 。 (另一方面,这几乎就像创建全局变量 :它几乎总是一个糟糕的主意。有一些Web框架,例如Wicket,它们大量使用它。像这样的Web框架在你使用时很糟糕需要同时做事并且让其他人使用非常不直观。 每个请求模型都有一个典型的Java 一个线程的趋势,如Play和Netty演示。不要被这种反模式所困扰。请谨慎使用ThreadLocal !这几乎总是设计糟糕的表现。)

您应该进一步了解ThreadLocal引起的内存泄漏并不总是被检测到。 通过扫描Web服务器的工作线程池以获取ThreadLocal变量来检测内存泄漏。 如果找到ThreadLocal变量,变量的Class显示其ClassLoader 。 如果此ClassLoader或其父项之一是刚刚取消部署的Web应用程序,则Web服务器可以安全地假设内存泄漏。

但是,假设您在ThreadLocal变量中存储了一些大型的String数组。 Web服务器如何假定此arrays属于您的应用程序? String.class当然是用JVM的引导程序ClassLoader实例ClassLoader ,不能与特定的Web应用程序相关联。 通过删除arrays,Web服务器可能会破坏在同一容器中运行的某些其他应用程序。 通过不删除它,Web服务器可能会泄漏大量内存。 (这次,它不是ClassLoader及其Class es泄漏。根据数组的大小,这种泄漏可能会更糟。)

它变得更糟。 这一次,假设您在ThreadLocal变量中存储了一个ArrayListArrayList是Java标准库的一部分,因此加载了系统ClassLoader 。 同样,没有办法告诉实例属于特定的Web应用程序。 但是,这次ClassLoader及其所有Classes都将泄漏,以及存储在线程本地ArrayList的此类类的所有实例。 这次,Web服务器甚至无法确定在发现ClassLoader没有被垃圾收集时发生内存泄漏,因为垃圾收集只能推荐给JVM(通过System#gc() )但不强制执行。

更新线程池并不像你想象的那么便宜。

无论何时取消部署应用程序,Web应用程序都不能只丢弃线程池中的所有线程。 如果在这些线程中存储了一些值,该怎么办? 当Web应用程序回收一个线程时,它应该(我不确定所有Web服务器是否都这样做)找到所有未泄漏的线程局部变量并在替换的Thread重新注册它们。 因此,您所说的关于效率的数字将不再适用。

同时,Web服务器需要实现一些管理替换所有线程池的Thread的逻辑,这对于您建议的时间计算都不起作用。 (您可能必须处理持久的请求 – 考虑在servlet容器中运行FTP服务器 – 这样这个线程池转换逻辑可能会在很长一段时间内处于活动状态。)

此外, ThreadLocal不是在servlet容器中创建内存泄漏的唯一可能性。

设置关闭挂钩是另一个例子。 (不幸的是,它是一个常见的。在这里,你应该在你的应用程序取消部署时手动删除关闭钩子。这个问题不会通过丢弃线程来解决。)关闭钩子是Thread的自定义子类的实例由应用程序的类加载器加载。

通常,任何保持对子类加载器加载的对象的引用的应用程序都可能会产生内存泄漏。 (这通常可以通过Thread#getContextClassLoader() 。)最后,开发人员不会导致内存泄漏,即使在Java应用程序中,许多开发人员误解了自动垃圾收集,因为没有内存泄漏 。 (想想Jochua Bloch 着名的堆栈实现示例 。)

在这个一般性陈述之后,我想评论一下Tomcat的内存泄漏保护:

Tomcat不承诺您检测所有内存泄漏,但涵盖了在wiki中列出的特定类型的此类泄漏。 Tomcat实际上做了什么:

检查JVM中的每个线程,并且内省Thread和ThreadLocal类的内部结构,以查看ThreadLocal实例或绑定到它的值是否由正在停止的应用程序的WebAppClassLoader加载。

某些版本的Tomcat甚至试图弥补泄漏:

Tomcat 6.0.24到6.0.26修改JDK(ThreadLocalMap)的内部结构以删除对ThreadLocal实例的引用,但这是不安全的(参见#48895),因此默认情况下它从6.0.27变为可选和禁用。 从Tomcat 7.0.6开始,池的线程将被更新,以便安全地修复泄漏

但是,您必须正确配置Tomcat才能执行此操作。 内存泄漏保护的wiki条目甚至警告你如何在涉及TimerThread时破坏其他应用程序,或者在启动自己的ThreadThreadPoolExecutor时如何泄漏内存泄漏,或者在使用多个Web应用程序的公共依赖项时。

Tomcat提供的所有清理工作都是最后的选择 ! 你想在生产代码中找不到任何东西。

总结 :创建内存泄漏不是Tomcat,它是您的代码。 某些版本的Tomcat尝试补偿此类泄漏,如果将其配置为可检测到这些泄漏。 但是,您有责任处理内存泄漏,并且您应该将Tomcat的警告视为修复代码的邀请,而不是重新配置Tomcat来清理您的混乱。 如果Tomcat在您的应用程序中检测到内存泄漏,甚至可能会有更多。 因此,从应用程序中获取堆和线程转储,并找出代码泄漏的位置 。