相关文章推荐
听话的显示器  ·  css ...·  1 年前    · 
坚强的玉米  ·  十个高级 TypeScript ...·  1 年前    · 

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;
}

发布于 2023-03-23 17:48 ・IP 属地江苏