8

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

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

我可能是错的(但如果没有错的可能性,就没有理由问):

  • 应用程序使用库
  • 图书馆使用ThreadLocal
  • ThreadLocal引用库中的对象
  • 每个对象都引用它的ClassLoader

网络服务器

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

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


我假设两件事:

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

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


作为警告,我们收到消息

Web 应用程序 ... 创建了一个 ThreadLocal ,其键类型为 ... ,值类型 ... 但在 Web 应用程序停止时未能将其删除。

这应该更好地表述为

Web 服务器 ... 使用线程池,但在停止(或重新部署)应用程序后未能更新它。

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

概括

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

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

这个问题的标题比正确更具有挑衅性,但它有它的意义。raphw 的回答也是如此。这个链接的问题有另一个开放的赏金。

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

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

这将确保这样的泄漏不存在超过delta最长请求的持续时间加上线程周转时间。费用构成如下:

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

如果最近没有取消部署或重新部署应用程序,只需设置阈值即可将其关闭。

4

1 回答 1

7

TL;DR 造成内存泄漏的不是 Web 服务器。是你。

让我首先更明确地说明问题:ThreadLocal变量通常指的Class是由 a 加载的 a的实例,该实例ClassLoader旨在由容器的应用程序独占使用。当此应用程序被取消部署时,ThreadLocal引用将成为孤立的。由于每个实例都保留对其的引用,Class并且由于每个实例都保留对其的引用,并且由于每个实例都Class保留对其曾经加载的所有类的引用,因此未部署的应用程序的整个类树无法进行垃圾收集,并且 JVM 实例遭受内存泄漏ClassLoaderClassLoader

查看此问题,您可以针对以下任一情况进行优化:

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

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

ThreadLocal但是,可以通过始终编写以下内容来避免诱发泄漏:

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

这样,你的线程总是会变成clean。(顺便说一句,这几乎就像创建全局变量一样:这几乎总是一个糟糕的主意。有一些 Web 框架,例如 Wicket,大量使用它。当你使用这样的 Web 框架时,使用起来很糟糕需要同时做一些事情并且对其他人使用变得非常不直观。有一种趋势,远离典型的 Java一个请求一个线程模型,例如用 Play 和 Netty 演示的。不要被这种反模式卡住。一定要ThreadLocal谨慎使用!这几乎总是糟糕设计的标志。)

您应该进一步注意,ThreadLocal并非总是检测到由 引起的内存泄漏。ThreadLocal通过扫描 Web 服务器的工作线程池中的变量来检测内存泄漏。如果ThreadLocal找到变量,则该变量会Class显示其ClassLoader. 如果这个ClassLoader或它的父级之一是刚刚取消部署的 Web 应用程序的父级,则 Web 服务器可以安全地假设内存泄漏。

但是,假设您将一些大型Strings 数组存储在一个ThreadLocal变量中。Web 服务器如何假定该数组属于您的应用程序?当然,它String.class是与 JVM 的引导实例一起加载的,ClassLoader并且不能与特定的 Web 应用程序相关联。通过删除阵列,Web 服务器可能会破坏在同一容器中运行的其他一些应用程序。如果不删除它,Web 服务器可能会泄漏大量内存。(这一次,泄漏的不是 aClassLoader和它的Classes。根据数组的大小,这种泄漏可能会更糟。)

而且情况会变得更糟。这一次,假设您ArrayListThreadLocal变量中存储了一个。它ArrayList是 Java 标准库的一部分,因此随系统一起加载ClassLoader。同样,没有办法告诉实例属于特定的 Web 应用程序。但是,这一次你ClassLoader和它的所有Classes内容以及存储在线程 local 中的此类类的所有实例都会泄漏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,并自检 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 条目甚至会警告您如何在涉及 s 时破坏其他应用程序,或者在启动您自己的 s 或s 或为多个 Web 应用程序使用公共依赖项时TimerThread如何泄漏内存泄漏。ThreadThreadPoolExecutor

Tomcat 提供的所有清理工作都是最后的手段!它没有你想在你的生产代码中拥有的东西。

总结:造成内存泄漏的不是 Tomcat,而是您的代码。某些版本的 Tomcat 会尝试补偿此类泄漏,如果配置为这样做,则可以检测到这些泄漏。但是,处理内存泄漏是您的责任,您应该将 Tomcat 的警告视为修复代码的邀请,而不是重新配置 Tomcat 以清理您的烂摊子。如果 Tomcat 在您的应用程序中检测到内存泄漏,则可能还有更多。因此,从您的应用程序中取出一个堆和线程转储,并找出您的代码在哪里泄漏

于 2013-10-30T21:11:52.177 回答