OutOfMemoryError on overprovisioned heap
Why am I getting the OutOfMemoryError when allocating a data structure that should happily fit within the heap I have provided for the JVM? This was a question I recently faced.
Indeed, when looking at what the developer was trying to accomplish and triple-checking the heap size given to the JVM via the -Xmx parameter, it indeed seemed that something shady was going on.
30 minutes later we understood the situation and solved the mystery. But it was indeed not obvious at the first place, so I thought it might save someone a day if I described the underlying problem in more details.
As always, the best way to understand a problem is via a hands-on example. I have constructed a small synthetic test case:
package eu.plumbr.demo; class ArraySize { public static void main(String... args) { int[] array = new int[1024*1024*1024]; } }
The code is simple – all it tries to do is to allocate an array with one billion elements. Now, considering that java int primitives require 4 bytes, one might think that running the code with 6g heap would run just fine. After all, those billion integers should consume only 4g memory. So why do I see the following when I execute the code?
My Precious:bin me$ java –Xms6g –Xmx6g eu.plumbr.demo.ArraySize Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at eu.plumbr.demo.ArraySize.main(ArraySize.java:6)
Before just tossing in even more heap (as a matter of fact, with –Xmx7g the example above runs just fine), let us try to understand why our expectation was wrong.
First – the int primitives in java do indeed require 4 bytes. So it is not like our JVM implementation has gone crazy over night. And I can assure you that the math is also correct – 1024*1024*1024 int primitives indeed would require 4,294,967,296 bytes or 4 gigabytes.
To understand what is happening, lets run the very same case and turn on garbage collection logging by specifying –XX:+PrintGCDetails:
My Precious:bin me$ java –Xms6g -Xmx6g -XX:+PrintGCDetails eu.plumbr.demo.ArraySize -- cut for brevity -- Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at eu.plumbr.demo.ArraySize.main(ArraySize.java:6) Heap PSYoungGen total 1835008K, used 125829K [0x0000000780000000, 0x0000000800000000, 0x0000000800000000) eden space 1572864K, 8% used [0x0000000780000000,0x0000000787ae15a8,0x00000007e0000000) from space 262144K, 0% used [0x00000007e0000000,0x00000007e0000000,0x00000007f0000000) to space 262144K, 0% used [0x00000007f0000000,0x00000007f0000000,0x0000000800000000) ParOldGen total 4194304K, used 229K [0x0000000680000000, 0x0000000780000000, 0x0000000780000000) object space 4194304K, 0% used [0x0000000680000000,0x0000000680039608,0x0000000780000000) PSPermGen total 21504K, used 2589K [0x000000067ae00000, 0x000000067c300000, 0x0000000680000000) object space 21504K, 12% used [0x000000067ae00000,0x000000067b087668,0x000000067c300000)
The answers are now staring right into our eyes: even though we have plenty of total heap available, no individual area in the heap is large enough to hold 4g of objects. Our 6g heap is divided into four separate regions, sized like this:
- Eden 1,536M
- Survivor spaces (from and to) 256M each
- OldGen 4,096M
Now, bearing in mind that object allocations must fit into a single region we indeed can see that the application stands no chance – there is just not enough room in any of our heap regions to accommodate this single 4g allocation.
So – is our only hope now to increase heap further? Even if we already have over-provisioned by nearly 50% – handing 6g of heap to a data structure which should fit into 4g? Not so fast – there is an alternative solution available. You can set the size of the different areas in memory. It is not as straightforward and user-friendly as one might expect, but two small modifications of the startup configuration will do the trick. When launching the same code with just two extra options:
My Precious:bin me$ java -Xms6g -Xmx6g -XX:NewSize=5g -XX:SurvivorRatio=10 eu.plumbr.demo.ArraySize
then the program does its job and no OutOfMemoryError is being thrown. Adding -XX:+PrintGCDetails to the startup also explains it:
Heap PSYoungGen total 4806144K, used 4369080K [0x00000006c0000000, 0x0000000800000000, 0x0000000800000000) eden space 4369408K, 99% used [0x00000006c0000000,0x00000007caaae228,0x00000007cab00000) from space 436736K, 0% used [0x00000007e5580000,0x00000007e5580000,0x0000000800000000) to space 436736K, 0% used [0x00000007cab00000,0x00000007cab00000,0x00000007e5580000) ParOldGen total 1048576K, used 0K [0x0000000680000000, 0x00000006c0000000, 0x00000006c0000000) object space 1048576K, 0% used [0x0000000680000000,0x0000000680000000,0x00000006c0000000) PSPermGen total 21504K, used 2563K [0x000000067ae00000, 0x000000067c300000, 0x0000000680000000) object space 21504K, 11% used [0x000000067ae00000,0x000000067b080c90,0x000000067c300000)
We see that the sizes of regions are now indeed what we asked for:
- Young size int total (eden + two survivor spaces) is 5g, as specified by our -XX:NewSize=5g parameter
- Eden is 10x larger than survivor, as we specified with the -XX:SurvivorRatio=10 parameter.
Note that in our case, both of the parameters were necessary. Specifying just the -XX:NewSize=5g would still split it between eden and survivors in a way where no individual area can hold the required 4g.
Hopefully reading this explanation will save you a day of debugging in the future. Or help you avoid over-provisioning the resources.
Reference: | OutOfMemoryError on overprovisioned heap from our JCG partner Vladimir Sor at the Plumbr Blog blog. |
What happens when the JVM tries to move this array to the old gen?