10

We have an application which serve images, to speed up the response time, we cache the BufferedImage directly in memory.

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

usage:

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();

    }
}

Note:the provider filed is a single instance.

However it seems that there is a possible memory leak because I will get Out Of Memory exception when the application keep running for about 2 minutes.

Then I use visualvm to check the memory usage:

enter image description here

Even I Perform GC manually, the memory can not be released.

And Though there are only 300+ BufferedImage cached, and 20M+ memory are used, 1.3G+ memory are retained. In fact, through "firebug" I can make sure that a generate image is less than 1Kb. So I think the memory usage is not healthy.

Once I do not use the cache (comment the following line):

//cacher.put(lkey, imageData);

The memory usage looks good:

enter image description here

So it seem that the cached BufferedImage cause the memory leak.

Then I tried to transform the BufferedImage to byte[] and cache the byte[] instead of the object itself. And the memory usage is still normal. However I found the Serialization and Deserialization for the BufferedImage will cost too much time.

So I wonder if you guys have any experience of image caching?


update:

Since there are so many people said that there is no memory leak but my cacher use too many memory, I am not sure but I have tried to cache byte[] instead of BufferedImage directly, and the memory use looks good. And I can not imagine 322 image will take up 1.5G+ memory,event as @BrettOkken said, the total size should be (256 * 256 * 4byte) * 322 / 1024 / 1024 = 80M, far less than 1Gb.

And just now,I change to cache the byte and monitor the memory again, codes change like this:

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

And the memory usage:

enter image description here

Same codes, same operation.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
hguser
  • 35,079
  • 54
  • 159
  • 293
  • Are the images color or grey scale? If grey scale, 8 or 16 bits per pixel? If color, what color model? What is the resolution of the images? To have an image that is less than 1 KB would indicate that it is 8 bit grey scale and smaller than 32 x 32 pixels. – Brett Okken Jul 10 '14 at 03:20
  • All of the generated images use type of `BufferedImage.TYPE_INT_ARGB`. The size is less than 1kb, because I just draw some strings. – hguser Jul 10 '14 at 03:23
  • 1
    BufferedImages are not compressed. They have backing arrays of data (likely a byte[] in your case) that will allocate value(s) per pixel based on color model (likely 4 in your case). So the amount of memory consumed is approximately width * height * 4. – Brett Okken Jul 10 '14 at 03:29
  • Then the memory used by 300+ `BufferedImage` seems to be normal, but the retained memory size are keeping increasing. – hguser Jul 10 '14 at 03:31
  • I think we have established that this is *not* a memory leak. Would it be possible to cache the output of your servlet (the full generated images), instead of the intermediate layers? Or is your data completely dynamic, so that each response will be unique? If you could cache the servlet response, you would probably both save CPU and memory. Perhaps add a HTTP cache (reverse-proxy or similar, like nginx or varnish), to offload JVM heap too. – Harald K Jul 12 '14 at 11:38
  • I agree with the others who say this doesn't look like a leak. It just seems you're not ever clearing the images out of your `cacher`. – Turix Jul 13 '14 at 01:15
  • ...but now you are putting PNG compressed byte arrays into the cache.. This is apples and oranges. Also, surprisingly high memory usage doesn't equal memory leak. – Harald K Jul 14 '14 at 19:05
  • @haraldK: Thanks for your attention. I can not cache the full servlet response, since the `layer` parameter can be different for different user and even different request. And response is And in fact I am not exactly sure if this is a memory leak that's why I use `possible` in the title of this question. And once I put the `PNG compressed byte arrays in the cache`, the memory usage is normal.So what confused me at the moment is that can the memory taken be so huge when "cache BufferedImage directly" compared with "cache the compressed byte array"? Since in my example? 1.2G vs 56M? – hguser Jul 15 '14 at 00:17
  • How about taking a heap dump and using something like Eclipse Memory Analyzer to figure out what the objects on the heap are and what is holding references to them. – Ryan Jul 18 '14 at 21:32
  • Did you manage to solve the problem? I got into the same situation and I believe there is certainly a leak. I could edit two 1K images and get to OutOfMemory. – Vojtěch Apr 12 '18 at 05:08
  • Also, for me, this did happen only on Linux Oracle implementation, didn't happen on OSX. – Vojtěch Apr 12 '18 at 05:09

3 Answers3

3

Note from both VisualVM screenshots that 97.5% memory consumed by 4,313 instances of int[] (Which I assume is by cached buffered image) is not consumed in non-cached version.

97.5% Memory Consumption

Although you have a less than 1K PNG image (which is compressed as per PNG format), this single image is being generated out of multiple instances of buffered image (which is not compressed). Hence you cannot directly co-relate image size from browser to memory occupied on server. So issue here is not memory leak but amount of memory required to cache this uncompressed layers of buffered images.

Strategy to resolve this is to tweak your caching mechanism:

  • If possible use compressed version of layers cached instead of raw images
  • Ensure that you will never run out of memory by limiting cache size by instances or by amount of memory utilized. Use either LRU or LIRS cache eviction policy
  • Use custom key object with coordinate and layer as two separate variables overriding with equals/hashcode to use as key.
  • Observe the behavior and if you have too many cache misses then you will need better caching strategy or cache may be unnecessary overhead.
  • I believe you are caching layers as you expect combinations of layer and coordinates and hence cannot cache final images but depending on kind of pattern of requests you expect you may want to consider that option if possible
Gladwin Burboz
  • 3,519
  • 16
  • 15
  • 3rd VisualVM screenshot you added is PNG compressed version of your raw BufferedImage and GC pattern for it seems to resolve your issue. However in long run you will still need to consider cache eviction policy. – Gladwin Burboz Jul 18 '14 at 22:37
0

Not sure what caching API you are using or what are actual values in your request. However based of visualvm it looks to me that String objects are leaking. Also as you mentioned if you turn off caching, problem is resolved.

Consider extract of below snippet of your code.

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

Now here are few things for you to consider for this code.

  • You possibly getting new string objects each time for lkey
  • Your cache has no upper limit with and no eviction policy (e.g. LRU)
  • Cacher instead of doing String.equals() is doing == and since this are new string objects they never match causing new entry each time
Gladwin Burboz
  • 3,519
  • 16
  • 15
  • I use `cache2k`:http://cache2k.org/ And I am sure the cached instance are less than 500. – hguser Jul 15 '14 at 00:08
  • On further analysis of two images of visualvm that you have provided, I take a step back on this answer and will be adding different answer. I no more suspect leaking String objects as an issue. – Gladwin Burboz Jul 15 '14 at 01:04
0

VisualVM is a start but it doesn't give the complete picture.

You need to trigger a heap dump while the application is using a high amount of memory. You can trigger a heap dump from VisualVM. It can also be done automatically on an OOME if you add this vmarg to the java process:

 -XX:+HeapDumpOnOutOfMemoryError 

Use Memory Analyzer Tool to open and inspect the heap dump.

The tool is quite capable and can help you walk the object references to discover:

  1. What is actually using your memory.
  2. Why the objects from #1 aren't being garbage collected.
Ryan
  • 2,061
  • 17
  • 28