In the previous act, we learned about java memory leaks and how memory is consumed . From the first act we learned about the separation of the java heap into multiple areas . Equipped with that knowledge, we can now have a look at the memory consumption during runtime and learn how to configure the JVM. While there are multiple memory regions managed by the JVM for different purposes, the one that is of most interest is the heap. In this post, I will describe the key elements of the heap and show how to have a look at it during runtime, using various JVM commands and especially the VisualVM.
Heap size: Minimum and Maximum
The memory management functionality is actually quite clever. It tries to avoid allocating too much memory from the operating system, but on the other hand it also tries to ensure that enough memory is available to the application.
To illustrate the behavior, I created a small Java program which wastes memory:
1ArrayList list = new ArrayList(); 2Thread.sleep(5000); // 5s to allow starting visualvm 3for (long l = 0; l < Long.MAX_VALUE; l++) { 4 list.add(new Long(l)); 5 for (int i = 0; i < 5000; i++) { 6 // busy wait, 'cause 1ms sleep is too long 7 if (i == 5000) break; 8 } 9}
Eventually this leads to: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Below are two graphs of the heap consumption monitored by VisualVM. The yellow area shows the available memory while the blue area shows the currently used memory. The graph sometimes decreases, because garbage collection frees some temporary objects used to increase the ArrayList size. The graph on the left hand side results from the application using the default settings of the JVM. You can clearly see that initially the JVM uses very few system resources and then starts to request more memory from the operating system as it is needed, finally ending at 128 megabytes. In the graph on the right hand side we see that the yellow area already covers 128MB from the start. This is because I added a JVM flag to change the default behavior. I will show you the flags in a second.
Adaptive Heap Size
Fixed Heap Size
One interesting thing to notice is: We get the OutOfMemoryError before the memory is fully depleted. You might wonder why. We will find out later, but start thinking about it.
Now, let us return to the flags I used. There are two flags to control the overall size of the JVM heap:
-Xmx128m
will set the maximum heap size to 128 megabytes-Xms128m
will set the initial/minimum heap size to 128 megabytes
While we may have an obvious reason for setting the maximum size to a value higher than the default (which varies based on the platform), there are two reasons for setting the minimum as well:
- Most Java applications are server type of applications, which run exclusively on a machine and are intended to use all the memory available. This reduces the minor overhead a dynamic resizing of the heap will impose.
- More important: Turning dynamic heap sizing off makes it much easier to understand and influence the internals of the heap, which I will talk about next. It also makes tuning effects more consistent over time and easier to perform.
Heap areas: Tenured, Young, and Permanent
The heap itself is separated into three areas: Tenured (also called Old), Young (also called New), and Permanent (Perm).
As already outlined in our first act, the idea behind that is to make garbage collection more efficient. Permanent should never require collection (although you can configure it), Old should be rarely collected, and Young will be collected a lot.
The Young area usually shows a lot of activity, so most garbage collection algorithms are designed to optimize collections in that area. The graph on the right hand side (taken from Oracle documentation ) shows how the age of objects is typically distributed. While most objects only live for a short time, there are also quite a few objects that stay alive a bit longer than 2-3 Young generation garbage collections (called minor collections). So the Young collection will be performed by saving the ones that survive in so-called survivor spaces. Objects stay in the survivor spaces until they have survived long enough to be promoted into the Old generation. Due to the way GC works, two Survivor Spaces are used and one of them is always empty. The remaining part of the Young generation is called Eden.
Gathering heap metrics
So how is the total memory distributed onto the different areas? The JVM offers us to keep track of a lot of information on memory via JMX. For example, there is a nice command line utility which shows you the usage of all your spaces:
1jstat
For my running Eclipse it looks like this:
1C:\Users\fla>jstat -gcutil 4188 2 S0 S1 E O P YGC YGCT FGC FGCT GCT 3 0,00 0,00 1,61 50,89 99,90 35 0,304 109 28,229 28,532
Ok, looks like the JVM is quite idle. Eden is almost empty (very low percentage), Old is filled about 50%. Perm is pretty full. The GC/GCT measurements are not important at the moment.
But what is the size of those spaces? jstat -gc
reveals this:
1C:\Users\fla>jstat -gc 4188 2S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 31984,0 1984,0 0,0 0,0 16256,0 263,6 40388,0 20553,3 55808,0 55752,7 35 0,304 133 34,558 34,861
Lots of numbers, all in kilobytes. Survivor Spaces are 2MB each, Eden is 16MB, Old is 40MB, Perm is 55MB. This comes close to the overall memory consumption of 135MB. The gap are the non heap memory areas.
All options and columns are explained in great detail in the official jstat documentation .
Configuring the heap
What can we configure, now that we know the occupation and sizes of the spaces? While the sizes offer a lot of configuration functionality, they are usually only touched when looking at garbage collection. All heap areas are equally good for storing data, but for garbage collection you might want to change some.
Without configuration, a client JVM uses this calculation for the spaces:
Heap = Tenured + Young
Tenured = 2 x Young
Young = Survivor x 2 + Eden
A common change is increasing the maximum Permanent generation size using -XX:MaxPermSize=128m
. The initial size can be set with -XX:PermSize=64m
.
Depending on the application, the New size can also be adjusted. This can be done as ratio -XX:NewRatio=2
(which is recommended, as it adjusts automatically), or as fixed size -XX:NewSize=128m
or -Xmn128m
(which is less flexible but easier to understand).
All ratios are configured as “one of me – N of the other”, where N is the value provided to the flag. For example, -XX:NewRatio=2
means 33% of the heap are for New and the remaining 66% are for Old.
Young space can be configured by using -XX:SurvivorRatio
to control the size of the survivor spaces, but this is often not very effective.
A typical configuration for a web application could look like this:
1-Xms2g -Xmx2g -XX:NewRatio=4 -XX:MaxPermSize=512m -XX:SurvivorRatio=6
The total memory used by Java would be slightly over 2.5GB. Old would be 1.6GB, Eden 300MB, and each Survivor Space 50MB.
Monitoring the Heap
Because the JVM is providing a lot of information on the heap, command line output is not the most convenient way to read it. In the screencast below, I will show you three tools for looking at the heap:
- JConsole , basic UI to read the JMX metrics provided by the JVM
- Visual GC , a great GC visualizer also available as plugin for VisualVM – suited best for on the spot analysis
- AppDynamics , providing long term monitoring for memory statistics
Why does OutOfMemory occur with free memory available?
You should now be able to answer the question we brought up in the beginning. Let’s have a look at the last snapshot of the Visual GC output:
The Old generation is full. Survivor Spaces are empty, and Eden has only 30%. Which means that we get an OutOfMemoryError even when we have still almost 30MB free memory in our Xmx limit of 128MB.
So why do we get the error at all? While we do not know exactly what’s going on, the code creates bigger and bigger arrays to hold the Long objects created in the loop.
A new Array is about to be created right here: at java.util.Arrays.copyOf(Arrays.java:2760)
. Now suppose that at some point there is not enough space in Eden for doing this (which means that we can assume that the array is larger than 22MB). A garbage collection should kick in and make some space available. However, there is no space left in Old, and the existing Eden objects cannot be moved anywhere. That’s when OutOfMemoryError is thrown.
To conclude, when you see an OutOfMemoryError it doesn’t mean that your whole heap memory has been exhausted. But you can be sure that at least one of the spaces is filled, preventing creation or movement of objects there.
In the next act, we will have a look at the actual objects stored in the heap by creating heap dumps.
More articles
fromFabian Lange
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Fabian Lange
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.