JVM memory in Tomcat, Solr and Java

How to size the heap, verify the configuration and reproduce a memory leak with a local exercise.

How much memory does the JVM need?

-Xms sets the initial heap size and -Xmx caps it. Objects live in the heap. Metaspace, thread stacks, direct buffers and other structures also consume memory. Leave headroom under the host RAM or container limit. For Solr, also leave room for Lucene’s filesystem cache.

Increase the Tomcat heap

If you start with catalina.sh, edit $CATALINA_BASE/bin/setenv.sh. Keep existing options and replace any old -Xms or -Xmx values. CATALINA_OPTS applies to the server process. Restart using your usual mechanism and check the new process. If the service invokes Java directly, edit its startup configuration.

CATALINA_OPTS="${CATALINA_OPTS:-} -Xms512m -Xmx2g"

Increase the Solr heap

On Linux, edit the solr.in.sh used by your service. Standard service installations typically use /etc/default/solr.in.sh. SOLR_HEAP sets both initial and maximum size. If you already use SOLR_JAVA_MEM, update that variable to avoid overlapping settings. Restart and check the effective values.

SOLR_HEAP="2g"

Tune a Java application

Put JVM options before -jar. The sizes are examples. With modern HotSpot in containers, an explicit -Xmx sets the cap. To size it from the memory limit the JVM detects, you can use -XX:MaxRAMPercentage=60.0 without setting -Xmx.

java -Xms256m -Xmx1g -jar app.jar

Check the heap and observe GC

jcmd -l lists visible Java processes. Replace 1234 with the PID and run jcmd as the service user in its environment. VM.flags shows effective values such as MaxHeapSize in bytes. GC.heap_info takes a heap snapshot. On Linux, ps reports RSS in KiB for the whole process.

jcmd -l
jcmd 1234 VM.flags
jcmd 1234 GC.heap_info
ps -p 1234 -o pid,rss,comm

With JDK 11 or later, add this startup option to capture rotating GC logs. Compare post-collection occupancy, pauses and latency under the same load before and after the change.

-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M

When memory keeps growing

If heap occupancy keeps growing after collections, investigate retained references. If the heap levels off but RSS rises, look at threads, buffers and native memory. Enable Native Memory Tracking at startup with -XX:NativeMemoryTracking=summary. Take a baseline and compare after applying load. NMT tracks internal HotSpot allocations and does not account for all RSS.

jcmd 1234 VM.native_memory baseline
jcmd 1234 VM.native_memory summary.diff scale=MB

A short JFR recording helps locate allocation sites. To investigate what keeps objects alive, analyse a heap dump. You can configure -XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath=/private/path with sufficient disk space. Dumps can contain application data, and capturing one live can cause a pause.

jcmd 1234 JFR.start name=memory settings=profile duration=60s filename=memory.jfr

Local object retention exercise

In your notes, CustomerManager returns the first customer with get(0), leaving it in the list. This version reproduces the difference with 1 MiB blocks. Save it as HeapRetention.java. In retain mode, the program keeps the references and eventually exhausts a 32 MiB heap. In drain mode, it removes each block from the queue so it can be collected.

import java.util.ArrayDeque;
import java.util.Queue;

class HeapRetention {
    public static void main(String[] args) {
        boolean retain = args.length > 0 && args[0].equals("retain");
        Queue<byte[]> pending = new ArrayDeque<>();

        for (int i = 0; i < 96; i++) {
            pending.add(new byte[1024 * 1024]);
            if (retain) {
                pending.element();
            } else {
                pending.remove();
            }
        }
        System.out.println("Pending blocks: " + pending.size());
    }
}

Run the modes separately. The first prints zero pending blocks. The second deliberately produces an OutOfMemoryError. A larger heap would delay the error while the queue keeps growing.

java -Xms32m -Xmx32m -Xlog:gc HeapRetention.java drain
java -Xms32m -Xmx32m -Xlog:gc HeapRetention.java retain

References