簡體   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