When people size a Java server they look at -Xmx. That sets the maximum heap, where your objects live. The operating system sees the whole process, and that is larger:

  • Heap: your objects, caches and sessions, up to -Xmx.
  • Metaspace: loaded classes. A framework-heavy application uses 100 to 300 MB.
  • Thread stacks: about 1 MB per thread by default, so 200 threads is 200 MB.
  • Code cache: compiled code, tens of MB.
  • Direct and native memory: network buffers, compression, JDBC drivers.
  • GC overhead: the collector's own bookkeeping.

As a working rule, the process uses the heap plus 300 to 600 MB. If Nginx, a database and the OS share the server, add those too.

The split I use

With a database on the same server I give 45% of RAM to the JVM heap, 15% to database buffers, and leave 40% for the OS, metaspace, thread stacks and file cache. Without a database the heap gets 60%.

Workload Heap Server
Small Spring Boot, Quarkus or Micronaut API 256 to 512 MB 2 GB
Tomcat with one or two WARs 512 MB to 1 GB 2 to 4 GB
Spring Boot with Hibernate, database on the same server 1 to 2 GB 4 to 8 GB
WildFly or Payara with one application 1 to 2 GB 4 to 8 GB

These are starting points. WildFly alone used about 450 MB on my test server before any application was deployed.

Measure, do not guess

Look at the heap after a full garbage collection: that is your live data, and the maximum should be at least 2.5 times that. Turn on GC logging and a dump on failure:

-Xlog:gc*:file=/var/log/myapp/gc.log:time,uptime:filecount=5,filesize=10m -XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError 

If the service disappears with no Java error in the log, the Linux OOM killer took it. Check dmesg -T | grep -i "killed process". Then the heap is too big for the machine, and raising it makes things worse.

I put the split into a small JVM heap calculator: enter RAM, app server and database, and it prints the -Xmx, -Xms, metaspace and database buffer values to paste into systemd.

The full version of this guide is here.