【问题标题】:Possible memory leak when caching BufferedImage缓存 BufferedImage 时可能存在内存泄漏
【发布时间】:2014-07-10 02:58:07
【问题描述】:

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

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

用法:

class ImageServlet  extends HttpServlet {
    void doGet(req,res){
        IData data= provider.render(req.getParameter("layers").split(","));

        OutputStream out=res.getOutputStream();
        data.save(out,"png")
        out.flush();

    }
}

注意:provider 字段是单个实例。

但似乎可能存在内存泄漏,因为当应用程序继续运行大约 2 分钟时,我会收到 Out Of Memory 异常。

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

即使我手动Perform GC,内存也无法释放。

虽然只有 300+ BufferedImage 被缓存,20M+ 内存被使用,1.3G+ 内存被保留。事实上,通过“萤火虫”我可以确保生成的图像小于1Kb。所以我认为内存使用不健康。

一旦我不使用缓存(注释以下行):

//cacher.put(lkey, imageData);

内存使用看起来不错:

看来缓存的BufferedImage 会导致内存泄漏。

然后我尝试将BufferedImage 转换为byte[] 并缓存byte[] 而不是对象本身。而且内存使用还是正常的。但是我发现SerializationDeserializationBufferedImage 会花费太多时间。

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


更新:

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

而刚才,我改为缓存byte,再次监控内存,代码变化如下:

BufferedImage ig = generateImage(layer,coordinate rwidth, rheight);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(ig, "png", bos);
imageData = bos.toByteArray();
tileCacher.put(lkey, imageData);

以及内存使用情况:

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

【问题讨论】:

  • 图片是彩色还是灰度?如果是灰度,每像素 8 位还是 16 位?如果颜色,什么颜色型号?图片的分辨率是多少?如果图像小于 1 KB,则表示它是 8 位灰度且小于 32 x 32 像素。
  • 所有生成的图片都使用BufferedImage.TYPE_INT_ARGB的类型。大小不到1kb,因为我只是画了一些字符串。
  • BufferedImages 未压缩。他们有数据的支持数组(在你的情况下可能是一个 byte[]),它将根据颜色模型(在你的情况下可能是 4 个)为每个像素分配值。所以消耗的内存量大约是宽*高*4。
  • 那么300+BufferedImage使用的内存好像是正常的,但是保留的内存大小一直在增加。
  • 我认为我们已经确定这不是内存泄漏。是否可以缓存 servlet 的输出(完整生成的图像)​​,而不是中间层?或者您的数据是完全动态的,因此每个响应都是独一无二的?如果您可以缓存 servlet 响应,您可能会同时节省 CPU 和内存。也许添加一个 HTTP 缓存(反向代理或类似的,如 nginx 或 varnish),也可以卸载 JVM 堆。

标签: java caching memory-leaks bufferedimage


【解决方案1】:

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

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

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

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

【讨论】:

  • 您添加的第三个 VisualVM 屏幕截图是原始 BufferedImage 和 GC 模式的 PNG 压缩版本,因为它似乎可以解决您的问题。但是从长远来看,您仍然需要考虑缓存驱逐策略。
【解决方案2】:

不确定您使用的是什么缓存 API,或者您的请求中的实际值是什么。然而,基于 visualvm,在我看来 String 对象正在泄漏。同样正如您提到的,如果您关闭缓存,问题就解决了。

考虑以下 sn-p 代码的摘录。

    String lkey = layer + "-" + coordinate;
    BufferedImage imageData = cacher.get(lkey);

现在,您需要为这段代码考虑几件事情。

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

【讨论】:

  • 我使用cache2k:cache2k.org 而且我确信缓存的实例少于500。
  • 在进一步分析您提供的两个 visualvm 图像时,我对这个答案退后一步,并将添加不同的答案。我不再怀疑泄漏 String 对象是一个问题。
【解决方案3】:

VisualVM 只是一个开始,但它并没有给出完整的画面。

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

 -XX:+HeapDumpOnOutOfMemoryError 

使用Memory Analyzer Tool 打开并检查堆转储。

该工具功能相当强大,可以帮助您遍历对象引用来发现:

  1. 究竟是什么在使用你的内存。
  2. 为什么 #1 中的对象没有被垃圾回收。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-25
    • 2012-09-23
    • 2020-06-12
    • 2011-07-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多