BufferedImage对象处理图片的OOM分析
•问题状况
应用服务端接收到上传图片的请求后,校验图片时发生OOM。(原因是使用BufferedImage对象判断图片是否CMYK模式时发生OOM,后经确认,业务上无需CMYK检查,目前删除代码解决)
•基础背景
- JPEG图片以24位颜色存储位图,支持高级别的压缩,压缩算法包括DCT变换、Zig-Zag扫描和Huffman编码等步骤。总之,主要损失高频部分的方式高压缩比,图片内容和质量影响压缩比,与文件大小显著相关。
- BMP图片采用位映射存储格式,除了图像深度可选以外,不采用其他任何压缩,独立定义每个像素的色彩值。可以说接近于原始的图片定义方式。文件内容包括:文件信息、位图信息、调色板(可选)、位图数据(像素数据),文件大小与内容无关,与尺寸有关。
- 一般图片文件的存储格式近似结构是:文件头(图片文件定义信息)、图片信息(图片尺寸等概要信息)、像素信息/像素段(图片像素信息)。
JPEG压缩率测试
同一纯色图片内容,分别保存为bmp和jpg,比较文件大小
结论:相同内容的jpg和bmp文件大小可以相差近200倍。
BufferedImage读取图片占用内存测试
通过BufferedImage对象循环读入相同内容(上述内容2)的多个图片文件到内存中,每读入一张图片记录heap使用量(单位M)。
代码:
BufferedImage-heap-test Expand source
private static List imgs = new LinkedList<>();
......
public void getImageSizeByBufferedImage(String src) {
File file = new File(src);
FileInputStream is = null;
try {
is = new FileInputStream(file);
BufferedImage sourceImg = null;
sourceImg = javax.imageio.ImageIO.read(is);
imgs.add(sourceImg);
} catch (Exception e) {
e.printStackTrace();
}
}
......
System.out.println((Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / (1024 * 1024));
heap使用量(程序输出原始数据从略,x-读入图片数量,y-heap使用量M):
现象:
- 读入单个jpg文件(22K)导致的内存增速均值:3.91M
- 读入单个bmp文件(3.8M)导致的内存增速均值:3.78M
- 读入jpg文件内存总量的变化曲线的起伏应该是由于gc引起,gc后内存总量二者几乎相同,可见总体增速与读入bmp文件相同,且都与图片数量正比。
- 扣除gc的影响,读入一个jpg图片的平均内存增速与其对应bmp的大小接近,可以认为在内存中,jpg图片被解码为类似bmp的按像素定义的格式。
- 另外读入一张jpg文件(681K内容1),发现heap在读入前后增长了142M,与其对应的bmp文件大小近似。
结论:
- BufferedImage读取图片是将整张jpg图片都读入内存,并解码,占用内存与文件大小和压缩率都成正比,一般jpg图片占用内存是其大小的是十数倍,并且这里200倍也未证明是最大倍数。
notice
Icon
理论上恶意用户只需要构造一张<3M的jpg图片就可以占用应用服务器600M的内存。
ImageReader读取图片信息占用内存测试
代码:
ImageReader-heap-test Expand source
if (src.indexOf("bmp") > 0) {//写法仅限测试
readers = ImageIO.getImageReadersByFormatName("bmp");
} else {
readers = ImageIO.getImageReadersByFormatName("jpg");
}
ImageReader reader = (ImageReader) readers.next();
ImageInputStream iis = ImageIO.createImageInputStream(file);
reader.setInput(iis, true);
imgs.add(reader);
reader.getWidth(0);
现象:内存增长不明显,执行速度快。
结论:ImageReader对象可以获取图片信息而不用把整张图片数据都缓存,只会试图解码文件的必要部分(图片概要区)去获得图片信息,而不用读取像素信息。
建议
- 在图片只需获取图片尺寸、格式等信息的场景下(比如文件上传时尺寸检查),使用ImageReader获取图片信息,所需内存空间和处理时间较少。
- 在需要修改图片内容(裁剪、缩放、水印等)的场景下,使用BufferedImage对象需要评估系统环境。比如在高并发的web应用中使用,需要在部署时高估服务器JVM的内存分配,并制定图片处理过程的并发策略。
- 另:对图片内容的处理就需要读写各像素的信息,可能应该需要解码文件流,且目前没有发现不依赖于此或者更高效的图片处理组件,暂无绕过方案,如有发现请后续补充。
建议公用取属性的公共方法示例:
public Map<String, Object> getPicWidthHeight(InputStream inputStream) {
Map<String, Object> picWidthHeightMap = new HashMap<String, Object>();
picWidthHeightMap.put("width", 0);
picWidthHeightMap.put("height", 0);
ImageInputStream iis = null;
ImageReader reader = null;
try {
iis = ImageIO.createImageInputStream(inputStream);
Iterator<ImageReader> iter = ImageIO.getImageReaders(iis);
reader = iter.next();
if (null != reader) {
reader.setInput(iis, true);
picWidthHeightMap.put("width", reader.getWidth(0));
picWidthHeightMap.put("height", reader.getHeight(0));
}
} catch (Exception e) {
// logger.logException(e);
} finally {
if (null != reader) {
reader.dispose();
}
if (null != iis) {
try {
iis.close();
} catch (IOException e1) {
// logger.logException(e1);
}
}
}
return picWidthHeightMap;
}