First of all, it is necessary to point out that the way Garbage Collection performs differs from JVM to JVM, so let's keep our attention on the Java HotSpot VM, the one you are probably using :)
As we all know, in Java it is not necessary to allocate and/or free memory, which makes life easier for the programmer but it can have impact on the responsiveness of the application.
An object is eligible for Garbage Collection as long as no reference points to it.
This leads us to rule N.1:
Free all references to an object as soon as it is no more needed
It is always said that the developer has no control on when the collection is executed; while this is true, it is also true that the developer has the power to determine
A more low level definition for rule N.1 could be: explicitly set to null references for not needed objects especially before the execution of any I/O operation, synchronized block or long iteration loops.
This is all to ensure the life of any object is as short as possible.
In the Java HotSpot VM the heap is split in 3 main areas:
- Young generation, further split into Eden space and 2 survivor spaces
- Tenured generation
- Permanent generation
Let's summarize how GC works:
- Every new object is allocated in the Eden space inside the Young generation
- When the Eden space is full, a minor collection is run. Unreferenced objects are discarded, live ones are moved to one of the survivor spaces
- As multiple minor collections run, objects that survive are moved multiple times from one survivor space to the other unless no space in the survivor space is left
- If no space remains in any survivor space, older objects are moved to the tenured generation
- When the tenured generation is filled as well a major collection occurs. All dead objects are removed from tenured generation
“Major collections usually last much longer than minor collections because a significantly larger number of objects are involved.” from the official GC tuning reference
In the ideal scenario minor collections should occur frequently and they should free most of the young generation; this is why it is essential objects live as less as possible.
I emphasize on this point because this is determined by the programmer, while the rest depends on configuration and on amount of Heap.
It would be easy to think then that, to get better performances, it would be enough to just reserve more memory for the heap: nothing more wrong!
Both minor and major collections completely freeze the JVM by pausing all running threads.
And since major collection are longer than minor, if the heap is bigger, the tenured generation will be bigger and your application will not be responsive for tens of seconds or even minutes!
So here it is rule N.2:
Never give more than 3 Gigabytes to the heap
If you really need to, you could configure 4 GB, but never more than that!
Tuning the JVM options is the best you can do. First of all you have to decide the algorithm that best suits you. Many choices are available but most of the time you will choose between:
- Parallel Collector: performs collections in parallel. It usually uses as many threads as the number of processors available on the machine. This algorithm can be set via the -XX:+UseParallelGC parameter
- Concurrent Collector (AKA Concurrent Mark Sweep,CMS): performs minor collections in parallel while major collections are performed concurrently to the application and therefore do not block it. It can be set using -XX:+UseConcMarkSweepGC.
First option is best for applications with high throughput (web applications for example), while the second is best if you cannot afford long pauses, usually for highly interactive applications.
Here it comes rule N.3:
Choose the Collector algorithm best for your application's type
However if CMS cannot free enough space a parallel major collection will run and this one will pause all threads.
CMS is also different in the sense that it is triggered when a certain percentage of the tenured generation gets filled.
Let's suppose the following is the case:
- you have configured the CMS to start once 80% of the Tenured generation is full using the ‑XX:CMSInitiatingOccupancyFraction=80 parameter.
- 81% of the Tenured generation is filled
- CMS runs and frees 20% of the memory
- In short time other 20% of the Tenured generation gets filled
- Another CMS will run
This would bring to rule 3-A:
Use the Parallel Collector if you are implementing a web application
In my experience following those simple rules should be enough in most of the cases!
However, if this is not the case many more options are available at the official Java HotSpot Options page.
Rule N.4 derives from this list:
Never set any JVM parameter randomly, but only after having profiled your application and having identified what the real problem is.
This rule could seem an obvious one, but unfortunately it is not followed most of the time :(
If you read along that huge list you will notice a lot of attention is given to String compression, as we all know strings use a lot of resources (and this is something absolutely not specific to Java) so setting those flags can probably help.
But most probably you are just using too many strings!
Most people use strings for everything while they could/should use some other data type!
Using the right data type for the right job is essential and it will not improve just performance :)
Rule N.5 would be:
Use String only if you really need to
One last parameter I would adjust is -XX:NewRatio, this one determines the ratio in size between tenured and young generations. If you are creating a lot of objects and their life is not too short but not too long either, set this parameter to make sure the Young generation is bigger.
In this way those objects are not moved to Tenured generation just before dying and ending up wasting heap resources.