Running a JVM application
The JVM defaults are chosen to work everywhere rather than to work well on your machine. Three decisions get most of the benefit.
Heap size
Set both ends and set them equal:
-Xms8g -Xmx8g
Equal values stop the JVM growing and shrinking the heap, which costs pauses for no benefit on a dedicated machine. Leave real room outside it: the JVM also uses memory for metaspace, thread stacks, code cache and direct buffers, so a 32 GB machine should not have a 30 GB heap.
A useful starting point is heap at roughly half of RAM for a server doing one job, then measure. Bigger is not automatically better — a larger heap means longer collections with some collectors.
Which collector
- G1 (default)
- Good general choice. Predictable pauses, scales to large heaps. Tune with
-XX:MaxGCPauseMillis=200and let it work out the rest. - ZGC
- Sub-millisecond pauses regardless of heap size, at some throughput cost. The right answer when latency percentiles matter more than raw throughput.
- Parallel
- Highest throughput, longest pauses. For batch work where nobody is waiting.
Keep GC logs
This is the setting people skip and then wish they had:
-Xlog:gc*:file=/var/log/myapp/gc.log:time,uptime:filecount=5,filesize=20M
When the application pauses mysteriously, the GC log tells you in seconds whether it was a collection, and whether the heap is filling faster than it is being emptied. A pattern of full collections that reclaim almost nothing is the signature of a leak. Rotate it — see log management.
Containers
Inside Docker, modern JVMs respect cgroup limits — but only if the limits are
set. Without a memory limit on the container, the JVM sees the whole machine and
sizes its heap accordingly, then gets killed. Set
-XX:MaxRAMPercentage=75 and a real container limit. See
Docker on your server.
Out of memory
Capture the evidence automatically:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/myapp/
-XX:+ExitOnOutOfMemoryError
The dump is what lets you find the leak afterwards. Exiting on OOM matters because a JVM that has run out of heap usually limps rather than stopping, and a process that has failed cleanly is restarted by systemd and visible in monitoring — a half-working one is not.
Live diagnosis
jcmd <pid> GC.heap_info — current heap state
jcmd <pid> Thread.print — a thread dump; three of these a
few seconds apart identify a deadlock or a stuck pool quickly
jcmd <pid> VM.native_memory summary — where memory outside
the heap has gone, if native tracking is enabled
On bare metal you also get consistent timings, which makes all of these numbers comparable between runs — see benchmarking a new server.
Still stuck? Mail support@novogara.com — an engineer answers, at any hour. Back to the knowledge base