在工作中,我们有一些运行多个 web 应用程序的 tomcat 服务器,其中大约一半必须进行一些图像处理。
在进行图像处理之前,这些网络应用会执行 ImageIO.scanForPlugins()
以将适当的图像读取器和写入器写入内存。在此之前,它只是在需要处理图像的任何时候运行,而现在我们仅在初始化 web 应用程序时运行扫描(因为我们在运行后不添加任何 jar,为什么要多次运行扫描?)
几天后,tomcat 实例因 OutOfMemoryError
而崩溃。幸运的是,我们设置了 HeapDumpOnOutOfMemoryError
选项,所以我查看了堆转储。在转储中,我发现 97% 的内存被 javax.imageio.spi.PartialOrderIterator
的实例占用。大部分空间被它的支持 java.util.LinkedList
占用,它有 1800 万个元素。链表由 javax.imageio.spi.DigraphNode
组成,其中包含由 ImageIO.scanForPlugins()
加载的图像读取器和写入器。
“啊哈”,我想,“我们一定是在某个地方循环运行扫描,我们只是一遍又一遍地添加相同的元素”。但是,我认为我应该仔细检查这个假设,所以我编写了以下测试类:
import javax.imageio.ImageIO;
public class ImageIOTesting {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
ImageIO.scanForPlugins();
if (i % 1000 == 0) {
System.out.println(Runtime.getRuntime().totalMemory() / 1024);
}
}
}
}
但是,当我在服务器环境中运行这个类时,使用的内存量从未改变!
快速浏览 javax.imageio 包的源代码可以看出,扫描会检查服务提供者是否已注册,如果已注册,它会先注销旧提供者,然后再注册新提供者。所以现在的问题是:为什么我有这个巨大的服务提供商链表?为什么它们存储为有向图?更重要的是,我该如何防止这种情况发生?
最佳答案
旧问题的迟到答案,但无论如何:
因为 ImageIO
插件注册表(IIORegistry
)是“VM global”,默认情况下它不能很好地与 servlet 上下文一起工作。如果您从 WEB-INF/lib
或 classes
文件夹加载插件,这一点尤其明显,正如 OP 似乎所做的那样。
Servlet 上下文动态加载和卸载类(每个上下文使用一个新的类加载器)。如果您重新启动您的应用程序,默认情况下旧类将永远保留在内存中(因为下一次 scanForPlugins
被调用时,它是另一个扫描/加载类的 ClassLoader
,因此它们将是注册表中的新实例。在测试代码循环中,始终使用相同的ClassLoader
,因此实例被替换,真正的问题从未显现)。
要解决此资源泄漏问题,我建议使用 ContextListener
来确保明确删除这些“上下文本地”插件。这是一个例子 IIOProviderContextListener
我在几个项目中使用过,它们实现了 ImageIO
插件的动态加载和卸载。
关于java - 如何解决以 ImageIO 插件为原因的 OutOfMemoryError?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12357969/