繁体   English   中英

缓存BufferedImage时可能发生内存泄漏

[英]Possible memory leak when caching BufferedImage

我们有一个提供图像的应用程序,为了加快响应时间,我们将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+内存。 事实上,通过“firebug”,我可以确保生成的图像小于1Kb 所以我认为内存使用不健康。

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

//cacher.put(lkey, imageData);

内存使用率看起来很好:

在此输入图像描述

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

然后我尝试将BufferedImage转换为byte[]并缓存byte[]而不是对象本身。 内存使用情况仍然正常。 但是我发现BufferedImageSerializationDeserialization Serialization Deserialization将花费太多时间。

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


更新:

既然有这么多人说没有内存泄漏,但是我的cacher使用了太多的内存,我不确定,但我试图直接缓存byte[]而不是BufferedImage ,内存使用看起来不错。 而我无法想象322图像会占用1.5G +内存,事件如@BrettOkken所说,总大小应为(256 * 256 * 4byte) * 322 / 1024 / 1024 = 80M 322/1024/1024 (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);

和内存使用情况:

在此输入图像描述

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

从两个VisualVM屏幕截图中注意到,4,313个int []实例消耗了97.5%的内存(我假设是通过高速缓存的缓存图像),非高速缓存版本不会消耗这些内存。

97.5%的内存消耗

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

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

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

不确定您使用的是哪种缓存API,或者您的请求中的实际值是什么。 但是基于visualvm,我认为String对象正在泄漏。 正如您所提到的,如果您关闭缓存,问题就解决了。

考虑下面代码片段的摘录。

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

现在,您可以在此处为此代码考虑一些事项。

  • 你每次为lkey获取新的字符串对象
  • 您的缓存没有上限和没有驱逐政策(例如LRU)
  • Cacher而不是做String.equals()正在做==因为这是新的字符串对象,它们永远不会匹配,每次都会导致新的条目

VisualVM是一个开始,但它没有提供完整的图片。

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

 -XX:+HeapDumpOnOutOfMemoryError 

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

该工具非常强大,可以帮助您遍历对象引用以发现:

  1. 什么是你的记忆。
  2. 为什么来自#1的对象没有被垃圾收集。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM