生产环境最近出现一个问题:聊天中发送图片会出现 OutOfMemoryError 报错,经过调查是服务端的图片压缩方法导致的。目前的图片压缩方法使用的是 Thumbnailator 库:Github地址。
问题调查
在本地打开 JProfile 工具,发现在上传问题图片时,会触发 Full GC,而触发 Full GC 会把程序的内存回收掉,应该不会触发 OutOfMemoryError。这里猜想可能在上传了问题图片后,图片压缩算法占用内存特别大,超出了 JVM 中设置的最大堆内存大小,并且这个内存短时间内回收不掉,所以导致了 OutOfMemoryError 报错。
在 官方给出的 FAQ 中找到了 thumbnailator 导致内存溢出的原因,如下:
文档中给出的解决办法是 增加 JVM 堆内存大小,或者使用 -Dthumbnailator.conserveMemoryWorkaround=true 来减少图片压缩造成的内存占用,两种办法都是通过调整 JVM 配置来处理。
处理办法
最简单的处理方式,直接增加 JVM 的堆内存大小。以测试环境为例,目前设置的堆内存大小是 512M
将这个大小提高至 1024M,重新对图片进行测试,上传成功。
FAQ 中提到:内存占用与图片的大小成正比,如果图片大小过大,那么还是会出现内存溢出的问题。
而我们在后台有对图片的大小做限制,生产环境中限制了最大上传图片大小为 14M,因此在本地使用一张 14M 的图片进行测试,没有发生内存溢出,上传成功。
其他处理办法
在 关于图片压缩的一些调查 这篇文章中提到了另外两款压缩图片的插件:ImageMagick、GraphicsMagick。
Thumbnailator 是通过将图片加载到内存的方式来完成对图片的处理,这会占用程序大量堆内存。而上面两个插件是通过在程序中调用第三方插件的方式来处理,图片的处理在第三方插件中进行,不会占用程序的内存。
Unsupported Image Type
在进行测试时,发现有一张图片报了 Unsupported Image Type 的错误,图片文件见附件。
查看了本地的文件系统,原图上传成功,压缩图没有上传,问题仍然是图片压缩算法导致的。
查阅了博客,原因是图片的色彩模式是 CMYK 导致的,常规的图片色彩模式都是 RGB 的,而 thumbnailator 不支持对 CMYK 模式的图片进行处理。
在官方给出的 FAQ 中也提到了这个问题,可以通过引入第三方依赖的方式来解决:
引入依赖:
1 | <dependency> |
之后再进行测试,又出现了 OutOfMemoryError 报错,仍然是图片压缩算法导致的。
并且 App 端在上传这张图片时,选择了这张图片后,需要等待很久才会有反应,见附件视频。
本来考虑对这类 CMYK 的图片,先由 App 端转成 RGB 再上传至服务端,但是 调查发现 CMYK 转成 RGB 后可能会出现颜色失衡的情况。并且测试了微信了聊天图片发送,发送这张图片并没有将 CMYK 转换成 RGB。
在上面的 FAQ 中提到可以使用 -Dthumbnailator.conserveMemoryWorkaround=true 来减少图片压缩造成的内存占用,在这里尝试加上
之后再进行测试,图片上传成功,缩略图也正常生成。
Dthumbnailator.conserveMemoryWorkaround 参数的作用
在 Thumbnailator 的源码中,可以找到关于这个参数的作用,如下:
1 | /* |
在 Issue#69 中也提到,只要触发了下面3个条件,就会对图片进行进一步压缩,防止其出现 OutOfMemoryError 的报错:
JVM以VM参数启动:-Dthumbnailator.Conservemoryworkaring = true- 高度和宽度的尺寸大于
1800像素 - 图片的预期大小 (
width * height * 4) 占用了JVM可用内存的1/4以上