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
- server.xml and Connectors
- Start, Stop, and Environment —
CATALINA_OPTS,setenv.sh - Nginx reverse proxy Tomcat — timeouts at the edge
What You Are Tuning
Client → Nginx → Connector (threads, queue) → Servlets → JVM heap (GC)| Layer | Symptom when starved |
|---|---|
| Connector threads | Requests wait; CPU not maxed |
| Accept queue | Connection refused or slow accept |
| JVM heap | OutOfMemoryError, long GC pauses |
| Nginx timeouts | 504 while Tomcat still working |
Tune with evidence—logs, curl timing, ss, not guesswork.
Connector Settings (server.xml)
Default HTTP Connector (simplified):
<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" />| Attribute | Default (typical) | Meaning |
|---|---|---|
maxThreads | 200 | Max worker threads handling requests |
acceptCount | 100 | Queue when all threads busy |
maxConnections | 8192 | Max concurrent connections (includes keep-alive) |
connectionTimeout | 20000 ms | Wait for request line on idle connection |
keepAliveTimeout | 60000 ms | Keep idle persistent connection open |
maxKeepAliveRequests | 100 | Max requests per keep-alive connection |
Code explanation:
- Each active request usually holds one thread until the response completes
- When
maxThreadsexhausted, new connections queue up toacceptCount, 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:
<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:
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:
export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -Dfile.encoding=UTF-8"| Flag | Meaning |
|---|---|
-Xms512m | Initial heap |
-Xmx1024m | Maximum heap |
-XX:+UseG1GC | G1 garbage collector (default on JDK 17+ in many installs) |
-Dfile.encoding=UTF-8 | Consistent encoding |
Code explanation:
-Xmsand-Xmxequal — avoids heap resize at runtime (common ops choice)- Heap too small →
java.lang.OutOfMemoryError: Java heap space - Heap too large on small VPS → long GC pauses, starving threads
Rough sizing guide (learning / small prod)
| VPS RAM | Tomcat -Xmx hint | Notes |
|---|---|---|
| 2 GB | 512m–768m | Leave room for OS, Nginx, MySQL |
| 4 GB | 1024m–1536m | One Tomcat instance |
| 8 GB | 2048m | Monitor 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:
# 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.
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
curlresponses under 100 ms- No OOM or 504 in logs
Do tune if:
- Load tests show queueing
OutOfMemoryErrorin logs- Nginx 502/504 with Tomcat threads at
maxThreads
Monitoring Habits (No Extra Tools)
# 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).txtProduction 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)
#!/bin/bash
export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Dfile.encoding=UTF-8"chmod +x $CATALINA_HOME/bin/setenv.sh
sudo systemctl restart tomcatVerify:
$CATALINA_HOME/bin/catalina.sh version
curl http://127.0.0.1:8080/hello/apiPost-Chapter Checklist
- Know
maxThreads,acceptCount,connectionTimeout - Set
CATALINA_OPTSheap with VPS headroom - Match Nginx
proxy_read_timeoutto slow endpoints - Avoid tuning without log/metric evidence
What Comes Next
| Chapter | Topic |
|---|---|
| 20 — Troubleshooting | OOM, thread exhaustion |
| 14 — Spring Boot WAR on Tomcat | Boot-specific limits |
| 18 — Project: Nginx + Tomcat | Full 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.