上下文
我正在审查服务器端应用程序中用于扩展图像的一些遗留Java代码。直到最近它主要用于具有1024x768或更低分辨率的输入图像,并且在这种情况下它似乎运行良好(或者至少足够好)。
然而,现在,许多被操作的图像的分辨率范围从2592x1936(8MP)到6000x4000(24MP)。这导致在服务器上频繁OutOfMemoryError
报告,我认为这是由图像处理代码引起的。
测试用例
我已经整理了一个简单的测试应用程序,它以与服务器相同的方式调用图像处理代码(有一点需要注意;测试用例是单线程的,而服务器环境显然不是这样):
public static void main(String[] args) throws Exception {
File file = new File("test_fullres.png"); //8MP test image, PNG format
for (int index = 0; index < 1000; index++) {
File output = new File("target/images/img_" + index + ".jpg");
verifyImageIsValidAndScale(file, output, 1024, 768, true, 80);
if (index % 10 == 0) {
String memStats = "Memstats after " + index + " iterations:";
memStats += "\tFree Memory: " + (Runtime.getRuntime().freeMemory() / 1024.0 / 1024.0) + " MBytes\n";
memStats += "\tTotal Memory: " + (Runtime.getRuntime().totalMemory() / 1024.0 / 1024.0) + " MBytes\n";
memStats += "\tMax Memory: " + (Runtime.getRuntime().maxMemory() / 1024.0 / 1024.0) + " MBytes\n";
System.out.println(memStats);
}
}
}
非常基本,只需循环1000次并在每次迭代时缩放输入图像,每10次迭代输出一些内存统计信息。我还设置了以下JVM标志:
-verbose:gc -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:GCPauseIntervalMillis=3000
对于控件,我使用1024x768 JPEG作为源输入。对于实际测试我输入的是8MP PNG图像。
代码
其他测试代码是:
public static boolean verifyImageIsValidAndScale(File in, File out, int width, int height, boolean aspectFit, int quality){
boolean valid = verifyImageIsValid(in);
aspectFitImage(in, out, width, height, quality);
return valid;
}
public static boolean verifyImageIsValid(File file){
InputStream stream = null;
try {
String path = file.getAbsolutePath();
stream = new FileInputStream(path);
byte[] buffer = new byte[11];
int numRead = stream.read(buffer);
if(numRead > 3 && buffer[1] == 'P' && buffer[2] == 'N' && buffer[3] == 'G') {
stream.close();
buffer = null;
LOG.debug("Valid PNG");
return ImageIO.read(new File(path)) != null; //PNG
} else if (numRead > 10 && ((buffer[6] == 'J' && buffer[7] == 'F' && buffer[8] == 'I' && buffer[9] == 'F' && buffer[10] == 0) ||
(buffer[6] == 'E' && buffer[7] == 'x' && buffer[8] == 'i' && buffer[9] == 'f' && buffer[10] == 0))){
stream.close();
buffer = null;
LOG.debug("Valid JPEG");
return ImageIO.read(new File(path)) != null; //JPEG
}
buffer = null;
} catch (Exception e) {
return false;
}
finally {
try {
stream.close();
}
catch (Exception ignored) {}
}
LOG.debug("Invalid Image");
return false;
}
public static Image aspectFitImage(File uploadedFile, File outputFile, int maxWidth, int maxHeight, int quality) {
if (quality < 0) {
quality = 0;
}
Image scaledImage = null;
try {
BufferedImage image = ImageIO.read(uploadedFile);
double scaleX = image.getWidth() > maxWidth ? maxWidth / (double)image.getWidth() : 1.0;
double scaleY = image.getHeight() > maxHeight ? maxHeight / (double)image.getHeight() : 1.0;
double useScale = scaleX < scaleY ? scaleX : scaleY;
scaledImage = image.getScaledInstance((int)(image.getWidth() * useScale), (int)(image.getHeight() * useScale), Image.SCALE_SMOOTH);
//XXX: getting bizarre results where overwriting a pre-existing output file doesn't properly overwrite the existing file; explicitly deleting the output file when it already exists seems to solve the issue
if (outputFile.exists()) {
outputFile.delete();
}
ImageOutputStream imageOut = ImageIO.createImageOutputStream(outputFile);
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
ImageWriteParam options = writer.getDefaultWriteParam();
options.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
options.setCompressionQuality(quality / 100.0f);
PixelGrabber pg = new PixelGrabber(scaledImage, 0, 0, -1, -1, true);
pg.grabPixels();
DataBuffer buffer = new DataBufferInt((int[]) pg.getPixels(), pg.getWidth() * pg.getHeight());
WritableRaster raster = Raster.createPackedRaster(buffer, pg.getWidth(), pg.getHeight(), pg.getWidth(), RGB_MASKS, null);
BufferedImage bi = new BufferedImage(RGB_OPAQUE, raster, false, null);
writer.setOutput(imageOut);
writer.write(null, new IIOImage(bi, null, null), options);
imageOut.close();
writer.dispose();
}
catch (Exception e) {
uploadedFile.delete();
return null;
}
return scaledImage;
}
此外,我发现如果我开始评论代码,直到剩下的唯一非常重要的行是:
BufferedImage image = ImageIO.read(uploadedFile);
......就最终记忆结果而言,结果仍然基本相同。
结果
下面是每次运行的最终输出,来自GC日志和内置内存日志记录语句:
#Source @ 1024x768 (control)
[GC pause (young) 1333M->874M(1612M), 0.0016051 secs]
Memstats after 990 iterations: Free Memory: 505.5521469116211 MBytes
Total Memory: 1612.0 MBytes
Max Memory: 6144.0 MBytes
#Source @ 2592x1936 (test):
[GC pause (young) 2161M->1610M(2632M), 0.0025038 secs]
Memstats after 990 iterations: Free Memory: 425.37239837646484 MBytes
Total Memory: 2632.0 MBytes
Max Memory: 6144.0 MBytes
两者似乎都应该高于。而这里没有捕获的是,在测试运行期间,垃圾收集器通常会一次回收高达1GB的内存。
也许值得注意的是,随着运行的进行,“总内存”值逐渐变得越来越高,好像垃圾收集器无法回收所有正在搅拌的内存。那么也许这意味着内存泄漏?
平均而言,垃圾收集器似乎每2-3次循环迭代触发一次。在任何情况下,由于我在单个线程上连续缩放图像,因此流失量似乎相当不合理。这不应该是烧掉千兆字节和千兆字节的内存。即使在幕后,Java正在将每个图像解压缩到32位位图,我也不希望内存使用率能够像观察到的那样快速攀升。
问题