TL;DR 造成内存泄漏的不是 Web 服务器。是你。
让我首先更明确地说明问题:ThreadLocal
变量通常指的Class
是由 a 加载的 a的实例,该实例ClassLoader
旨在由容器的应用程序独占使用。当此应用程序被取消部署时,ThreadLocal
引用将成为孤立的。由于每个实例都保留对其的引用,Class
并且由于每个实例都保留对其的引用,并且由于每个实例都Class
保留对其曾经加载的所有类的引用,因此未部署的应用程序的整个类树无法进行垃圾收集,并且 JVM 实例遭受内存泄漏。ClassLoader
ClassLoader
查看此问题,您可以针对以下任一情况进行优化:
- 即使在整个重新部署过程中,也允许每秒尽可能多的请求(从而缩短响应时间并重用线程池中的线程)
- 确保线程在重新部署发生时通过丢弃线程保持清洁(因此补丁忘记了手动清理)
大多数 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 服务器可以安全地假设内存泄漏。
但是,假设您将一些大型String
s 数组存储在一个ThreadLocal
变量中。Web 服务器如何假定该数组属于您的应用程序?当然,它String.class
是与 JVM 的引导实例一起加载的,ClassLoader
并且不能与特定的 Web 应用程序相关联。通过删除阵列,Web 服务器可能会破坏在同一容器中运行的其他一些应用程序。如果不删除它,Web 服务器可能会泄漏大量内存。(这一次,泄漏的不是 aClassLoader
和它的Class
es。根据数组的大小,这种泄漏可能会更糟。)
而且情况会变得更糟。这一次,假设您ArrayList
在ThreadLocal
变量中存储了一个。它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
如何泄漏内存泄漏。Thread
ThreadPoolExecutor
Tomcat 提供的所有清理工作都是最后的手段!它没有你想在你的生产代码中拥有的东西。
总结:造成内存泄漏的不是 Tomcat,而是您的代码。某些版本的 Tomcat 会尝试补偿此类泄漏,如果配置为这样做,则可以检测到这些泄漏。但是,处理内存泄漏是您的责任,您应该将 Tomcat 的警告视为修复代码的邀请,而不是重新配置 Tomcat 以清理您的烂摊子。如果 Tomcat 在您的应用程序中检测到内存泄漏,则可能还有更多。因此,从您的应用程序中取出一个堆和线程转储,并找出您的代码在哪里泄漏。