Java 缓存BuffereImage时可能发生内存泄漏

Java 缓存BuffereImage时可能发生内存泄漏,java,caching,memory-leaks,bufferedimage,Java,Caching,Memory Leaks,Bufferedimage,我们有一个提供图像服务的应用程序,为了加快响应时间,我们将buffereImage直接缓存在内存中 class Provider { @Override public IData render(String... layers,String coordinate) { int rwidth = 256 , rheight = 256 ; ArrayList<BufferedImage> result = new ArrayList&l

我们有一个提供图像服务的应用程序,为了加快响应时间,我们将
buffereImage
直接缓存在内存中

class Provider {
    @Override
    public IData render(String... layers,String coordinate) {
        int rwidth = 256 , rheight = 256 ;

        ArrayList<BufferedImage> result = new ArrayList<BufferedImage>();

        for (String layer : layers) {
            String lkey = layer + "-" + coordinate;
            BufferedImage imageData = cacher.get(lkey);
            if (imageData == null) {
                try {
                    imageData = generateImage(layer, coordinate,rwidth, rheight, bbox);
                    cacher.put(lkey, imageData);
                } catch (IOException e) {
                    e.printStackTrace();
                    continue;
                }
            }

            if (imageData != null) {
                result.add(imageData);
            }

        }
        return new Data(rheight, rheight, width, result);
    }

    private BufferedImage generateImage(String layer, String coordinate,int rwidth, int rheight) throws IOException {
        BufferedImage image = new BufferedImage(rwidth, rheight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = image.createGraphics();
        g.setColor(Color.RED);
        g.drawString(layer+"-"+coordinate, new Random().nextInt(rwidth), new Random().nextInt(rheight));
        g.dispose();
        return image;
    }

}
class Data implements IData {
    public Data(int imageWidth, int imageHeight, int originalWidth, ArrayList<BufferedImage> images) {
        this.imageResult = new BufferedImage(this.imageWidth, this.imageHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = imageResult.createGraphics();
        for (BufferedImage imgData : images) {
            g.drawImage(imgData, 0, 0, null);
            imgData = null;
        }
        imageResult.flush();
        g.dispose();

        images.clear();
    }

    @Override
    public void save(OutputStream out, String format) throws IOException {
        ImageIO.write(this.imageResult, format, out);
        out.flush();
        this.imageResult = null;
    }
}
注意:
提供程序
字段是一个实例

但是,似乎存在可能的内存泄漏,因为当应用程序持续运行大约2分钟时,我将出现
内存不足
异常

然后我使用
visualvm
检查内存使用情况:

即使我手动执行GC,内存也无法释放

虽然只有300+
buffereImage
缓存,并且使用了
20M+
内存,但仍保留了
1.3G+
内存。事实上,通过“firebug”,我可以确保生成的图像小于
1Kb
。所以我认为内存使用是不健康的

一旦我不使用缓存(在下面的行中注释):

内存使用情况看起来不错:

因此,缓存的
buffereImage
似乎导致了内存泄漏

然后我尝试将
BufferedImage
转换为
byte[]
并缓存
byte[]
,而不是对象本身。内存使用仍然正常。但是,我发现
缓冲区映像的
序列化
反序列化
将花费太多时间

所以我想知道你们是否有图像缓存的经验


更新:

因为有很多人说没有内存泄漏,但我的缓存使用了太多内存,我不确定,但我已经尝试缓存
byte[]
而不是直接缓存
BufferedImage
,内存使用情况看起来不错。我无法想象322图像会占用1.5G+内存,正如@BrettOkken所说,总大小应该是
(256*256*4字节)*322/1024/1024=80M
,远小于1Gb

刚才,我更改为缓存
字节
并再次监视内存,代码更改如下:

BufferedImage ig = generateImage(layer,coordinate rwidth, rheight);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(ig, "png", bos);
imageData = bos.toByteArray();
tileCacher.put(lkey, imageData);
以及内存使用情况:


相同的代码,相同的操作。

不确定您使用的缓存API是什么,或者请求中的实际值是什么。然而,基于visualvm,我认为字符串对象正在泄漏。正如您所提到的,若关闭缓存,问题就会得到解决

考虑以下代码片段的摘录

    String lkey = layer + "-" + coordinate;
    BufferedImage imageData = cacher.get(lkey);
现在这里有几件事让你考虑一下这个代码。

  • 您可能每次都会为lkey获取新的字符串对象
  • 您的缓存没有上限,也没有逐出策略(例如LRU)
  • Cacher而不是doing String.equals()是doing==和,因为 它们从不匹配的新字符串对象是否每次都会导致新条目

请注意,从两个VisualVM屏幕截图中可以看出,4313个int[]实例(我假设是缓存的图像)消耗的97.5%内存在非缓存版本中没有消耗

虽然您有一个小于1K的PNG图像(按照PNG格式进行压缩),但这个图像是由缓冲图像(未压缩)的多个实例生成的。因此,您不能直接将浏览器中的图像大小与服务器上占用的内存关联起来。所以这里的问题不是内存泄漏,而是缓存这些未压缩的缓冲图像层所需的内存量

解决此问题的策略是调整缓存机制:

  • 如果可能,请使用缓存层的压缩版本,而不是原始版本 图像
  • 通过限制缓存大小,确保永远不会耗尽内存 按实例或所使用的内存量。使用LRU或LIR 缓存逐出策略
  • 将坐标和图层作为两个单独的对象使用自定义关键点对象 使用equals/hashcode重写变量以用作键
  • 观察该行为,如果缓存未命中过多,则 需要更好的缓存策略,否则缓存可能是不必要的 头顶
  • 我相信您正在缓存层,正如您所期望的层的组合 和坐标,因此无法缓存最终图像,但取决于 您期望的请求模式可能会考虑该选项,如果可能的话

VisualVM是一个开始,但它没有给出完整的图片

当应用程序使用大量内存时,需要触发堆转储。 您可以从VisualVM触发堆转储。如果将此vmarg添加到java进程中,也可以在OOME上自动完成:

 -XX:+HeapDumpOnOutOfMemoryError 
用于打开和检查堆转储

该工具功能强大,可帮助您浏览对象引用以发现:

  • 究竟是什么在使用你的记忆
  • 为什么#1中的对象没有被垃圾收集

  • 图像是彩色的还是灰度的?如果是灰度,每像素8或16位?如果是颜色,是什么颜色的型号?图像的分辨率是多少?如果图像小于1 KB,则表示该图像为8位灰度且小于32 x 32像素。所有生成的图像都使用
    缓冲图像类型。type_INT_ARGB
    。大小小于1kb,因为我只画了一些字符串。BuffereImage没有压缩。它们有支持数据数组(在您的情况下可能是字节[]),根据颜色模型(在您的情况下可能是4)为每个像素分配值。因此,消耗的内存量大约为宽*高*4。然后300+
    BuffereImage
    使用的内存似乎正常,但保留的内存大小一直在增加。我认为我们已经确定这不是内存泄漏。是否可以缓存servlet的输出(完整生成的图像),而不是中间层?或者您的数据是完全动态的,因此每个响应都是唯一的?如果可以缓存servlet响应,那么可能既可以节省CPU又可以节省内存。也许还可以添加一个HTTP缓存(反向代理或类似的,如nginx或varnish)来卸载JVM堆 String lkey = layer + "-" + coordinate; BufferedImage imageData = cacher.get(lkey);
     -XX:+HeapDumpOnOutOfMemoryError