Kingsfordweg 151, 1043GR Amsterdam, The Netherlands ● 24/7 support — support@novogara.com
Knowledge base

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=200 and 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