JVM and Connector Tuning (Intro)

Introduction

Default Tomcat settings run fine for tutorials and light traffic. Under load, thread limits, connection queues, and JVM heap become bottlenecks—symptoms include slow responses, 504 timeouts from Nginx, and OutOfMemoryError in logs/catalina.*.log. This chapter introduces Connector knobs in server.xml and JVM flags via CATALINA_OPTS—enough to size a small VPS, not a full performance consultancy.

Prerequisites

What You Are Tuning

text
  Client → Nginx → Connector (threads, queue) → Servlets → JVM heap (GC)
LayerSymptom when starved
Connector threadsRequests wait; CPU not maxed
Accept queueConnection refused or slow accept
JVM heapOutOfMemoryError, long GC pauses
Nginx timeouts504 while Tomcat still working

Tune with evidence—logs, curl timing, ss, not guesswork.

Connector Settings (server.xml)

Default HTTP Connector (simplified):

xml
<Connector port="8080" protocol="HTTP/1.1"
           address="127.0.0.1"
           connectionTimeout="20000"
           redirectPort="8443"
           maxThreads="200"
           acceptCount="100"
           maxConnections="8192"
           keepAliveTimeout="60000"
           maxKeepAliveRequests="100" />
AttributeDefault (typical)Meaning
maxThreads200Max worker threads handling requests
acceptCount100Queue when all threads busy
maxConnections8192Max concurrent connections (includes keep-alive)
connectionTimeout20000 msWait for request line on idle connection
keepAliveTimeout60000 msKeep idle persistent connection open
maxKeepAliveRequests100Max requests per keep-alive connection

Code explanation:

  • Each active request usually holds one thread until the response completes
  • When maxThreads exhausted, new connections queue up to acceptCount, then OS may reject
  • CPU-bound apps: more threads ≠ faster—threads pile up waiting for CPU

Modest increase for a busy API

After backup server.xml:

xml
<Connector port="8080" protocol="HTTP/1.1"
           address="127.0.0.1"
           maxThreads="300"
           acceptCount="200"
           connectionTimeout="20000" />

Restart Tomcat. Re-test under load; watch logs/ and CPU.

Warning

Do Not Set maxThreads to Thousands

200–400 is common on a 2–4 vCPU VPS. Thousands of threads increase context switching and memory without benefit.

Align Nginx Timeouts

If Tomcat legitimately runs 90s reports, Nginx must wait long enough:

nginx
proxy_read_timeout 120s;
proxy_connect_timeout 60s;
proxy_send_timeout 120s;

Otherwise Nginx returns 504 Gateway Timeout while Tomcat still processes—misleading for debugging.

JVM Heap via CATALINA_OPTS

Set in bin/setenv.sh:

bash
export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -Dfile.encoding=UTF-8"
FlagMeaning
-Xms512mInitial heap
-Xmx1024mMaximum heap
-XX:+UseG1GCG1 garbage collector (default on JDK 17+ in many installs)
-Dfile.encoding=UTF-8Consistent encoding

Code explanation:

  • -Xms and -Xmx equal — avoids heap resize at runtime (common ops choice)
  • Heap too smalljava.lang.OutOfMemoryError: Java heap space
  • Heap too large on small VPS → long GC pauses, starving threads

Rough sizing guide (learning / small prod)

VPS RAMTomcat -Xmx hintNotes
2 GB512m–768mLeave room for OS, Nginx, MySQL
4 GB1024m–1536mOne Tomcat instance
8 GB2048mMonitor actual usage

Leave headroom—MySQL or other services share the machine on typical Hello Code stacks.

View memory in logs

On startup, Tomcat logs JVM memory lines. Also:

bash
# Linux: find Tomcat Java PID
pgrep -f catalina
ps -o pid,rss,vsz,cmd -p <PID>

RSS — real memory footprint approximate.

Thread Pool vs Heap Interactions

  • Each thread has a stack (default often ~1 MB max stack reserved per thread on some JVMs—actual use lower)
  • 1000 threads + large heap on 2 GB RAM → swapping and death
  • Prefer fixing slow SQL, caching, async (framework-level) before maxing threads

Spring Boot embedded Tomcat uses similar server.tomcat.max-threads in application.yml—same concepts.

Keep-Alive and Reverse Proxies

HTTP/1.1 keep-alive reuses connections between Nginx and Tomcat—good for latency.

xml
maxKeepAliveRequests="100"
keepAliveTimeout="60000"

Nginx also keeps proxy_http_version 1.1 to upstream. Mismatch rarely bites on localhost—more important under high RPS.

When Not to Tune

Skip deep tuning if:

  • Tutorial WAR with single user
  • curl responses under 100 ms
  • No OOM or 504 in logs

Do tune if:

  • Load tests show queueing
  • OutOfMemoryError in logs
  • Nginx 502/504 with Tomcat threads at maxThreads

Monitoring Habits (No Extra Tools)

bash
# Active connections to 8080
ss -tlnp | grep 8080
ss -tan | grep :8080 | wc -l
 
# Tail errors
grep -i "OutOfMemory\|SEVERE" $CATALINA_HOME/logs/catalina.$(date +%Y-%m-%d).log
 
# Request timing from access log (if AccessLogValve has %D)
tail -5 $CATALINA_HOME/logs/localhost_access_log.$(date +%Y-%m-%d).txt

Production teams add Micrometer, Prometheus, Grafana via Spring Boot Actuator—standalone Tomcat can use JMX or external agents (out of scope here).

AJP and Native Connectors

APR/native and AJP tuning are legacy/advanced. HTTP proxy_pass from Nginx to 127.0.0.1:8080 is the Hello Code default—focus on HTTP Connector and heap.

Example setenv.sh (Small VPS)

bash
#!/bin/bash
export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -Dfile.encoding=UTF-8"
bash
chmod +x $CATALINA_HOME/bin/setenv.sh
sudo systemctl restart tomcat

Verify:

bash
$CATALINA_HOME/bin/catalina.sh version
curl http://127.0.0.1:8080/hello/api

Post-Chapter Checklist

  • Know maxThreads, acceptCount, connectionTimeout
  • Set CATALINA_OPTS heap with VPS headroom
  • Match Nginx proxy_read_timeout to slow endpoints
  • Avoid tuning without log/metric evidence

What Comes Next

ChapterTopic
20 — TroubleshootingOOM, thread exhaustion
14 — Spring Boot WAR on TomcatBoot-specific limits
18 — Project: Nginx + TomcatFull stack exercise

FAQ

Same -Xmx for Spring Boot JAR and Tomcat?

Concept yes—one JVM process. Standalone Tomcat hosting multiple WARs shares one heap across all apps.

maxThreads 200 not enough?

Find slow operations first (DB, external APIs). Blindly raising threads worsens pile-ups.

PermGen / Metaspace errors?

Classloader leaks from hot redeploys—restart Tomcat; disable reloadable in production; increase -XX:MaxMetaspaceSize only with diagnosis.

G1 vs ZGC?

JDK 21: G1 is fine for typical web loads. ZGC for very large heaps/low latency—advanced topic.

Connector port vs threads?

Independent—port is listen socket; threads handle work after accept.

Package Tomcat setenv?

Use /etc/default/tomcat10 or systemd Environment=—same variables, distro-specific file.