Java Deflater and OutOfMemoryError

By on Aug 2, 2011 in eng | 5 comments

Share On GoogleShare On FacebookShare On Twitter

Summary: Relying on Finalizer to clean up native memory of Deflater object may cause OutOfMemoryError

I guess this is old for many developers but it is new to me. I have been asked to join an investigation of OutOfMemoryError issue occurred in a project outside my division. Actually, it was a brief involvement. My colleague who was handling the issue already got a possible root cause. He believed that it’s something about the compressing code that just had been added in the newest release. The compressing logic used java.util.zip.Deflater and the end() method of Deflater objects hadn’t been explictly closed once the compressing was done. Cleaning up mechanism relied on Java Finalizer to call finalize() method of Deflater objects

My colleague suspected that a Deflater object consumed a little space in Java heap but also allocated much bigger space of native memory. He believed it was possible that a large number of Deflater objects had been created which caused OOM in native side. Most of the Deflater objects were unreachable but GC didn’t kick in because overall heap consumption was still low. The result was that all those unreachable objects hadn’t been scheduled for Finalizer yet so all the native memory allocations were still retained

I was skeptical of his assumption at first. I thought if JVM was being under the risk of OOM then way didn’t GC try to reclaim java heap to let the object that allocated native memory be cleaned up. I decided to write a simple code to prove the scenario


public static void main(String[] args) throws IOException {
        int testSize = 1;
        if(args.length > 0){ testSize = Integer.parseInt(args[0]); }

        String content = readConent( Main.class.getResourceAsStream("data.txt"));
        byte[] input = content.getBytes("UTF-8");

        System.out.println("Test size = " + testSize);
        for(int i=0; i< testSize; i++){
            byte[] output = new byte[500];
            Deflater deflater = new Deflater();
            deflater.setInput(input);
            deflater.finish();
            deflater.deflate(output);
            //deflater.end(); 
        }

        System.out.println("Done");

        //Let it run for a while so I can monitor the java process.
        try{ Thread.sleep(60*60*1000); }catch(InterruptedException ex){}
    }

The program repeatedly compresses a short string (around 300 bytes). Notice that the line deflater.end() has been commented out which will defer the cleaning up process until the Finalizer thread start looking at the objects

I got OOM when I ran the program to create 6300 instances of Deflater

java -cp build\classes -verbose:gc -Xmx300m -Xms300m deflat.Main 6300

Exception in thread “main” java.lang.OutOfMemoryError
at java.util.zip.Deflater.init(Native Method)
at java.util.zip.Deflater.(Deflater.java:123)
at java.util.zip.Deflater.(Deflater.java:140)
at deflat.Main.main(Main.java:23)

I ran the program with –verbose:gc to check whether GC would try to collect objects in heap before OOM occurred or not. There was no GC activity logged to console at all. My colleague’s assumption was right; GC are not able to aware that native memory usage is about to exceed limit

I ran the program again with test size of 6200 to let it finish execution successfully then checked the current heap size

“C:\Program Files\Java\jdk1.6.0_13\bin\jstat.exe” -gc 2936

S0C          S1C        S0U    S1U   EC              EU          OC                OU
2304.0    2304.0   0.0     0.0     19008.0   4186.7   283584.0     0.0

Only the Eden part of heap size was occupied and the current utilization was just 4186.7 KB (EU column). The size of heap usage wasn’t large enough to trigger any GC activities it wasn’t even large enough for a minor collection in new generation. It was safe to say that the OOM generated with test size of 6300 wasn’t the result of exhausted java heap

While the java process with ID 2936 was still running, I could see in Window Task Manager that the virtual memory of the process was almost 1.9 GB. I guess the OOM from the test size of 6300 is about the memory limit of 32bit process which practically around 2 GB (the rest 2 GB is reserved for Window).

Clean it up as soon as possible

I found a very informative link described this GC behavior. One of SUN engineers has clarified that:

We’re throwing an OOM not because the java heap is (anywhere close to) full,but because the non-java (i.e., C/C++) heap is full: you can get an OOM when either heap is full. The reason for the OOM is because unreachable, but not yet finalized, java objects are holding pointers to C/C++ heap memory, and that memory is deallocated pnly when the finalizers run. The JLS doesn’t specify when finalizers get run, so if the VM delays executing them long enough, the C/C++ heap memory doesn’t get freed soon enough to prevent an OOM

An easy fix for the problem is to clean up the Deflater objects as soon as possible by calling deflater.end() method once the compression is done. The modified version of the program can run with test size of 10000 or more. The interesting thing is that with the test size of 10000, the heap utilization is around 6.4 MB which is still not enough for a minor GC collection so all 10000 Deflater objects are still there in heap but the virtual memory of the Java process is just around 331 MB. It seems like the end() method has cut the link between the a Deflater object and the space it has allocated in native memory

Note: HeapDumpOnOutOfMemoryError

I have tried using -XX:+HeapDumpOnOutOfMemoryError JVM options to get heap dump right at the point of OOM but it doesn’t work so I need to lower the test size a bit to keep the process alive and monitor the process using jstat instead. I found an explanation saying that:

Allocation of a native thread is not from the heap or the permanent generation – hence failure to allocate one does not result in a heap dump. (There would be little point in dumping the heap as that is not what was exhausted.)

The OOM caused by native memory usage will not trigger the JVM option

5 Comments

  1. Thitthiphong

    August 2, 2011

    That is the great topic

  2. chatchai

    August 2, 2011

    @Thitthiphong thanks very much krub.

  3. roofimon

    September 29, 2011

    You are the man

  4. pramoth

    September 29, 2011

    Great topic and great analytic.

  5. genius

    January 30, 2012

    They should add this API in JNI:

    env->addMemoryPressure(long)
    env->removeMemoryPressure(long)

    Which would give a hint to JVM to schedule GC more often because native heap memory should be involved in GC schedule heuristics too.

    For now, I think this API can be emulated via JNI itself.

Submit a Comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>