How to Supercharge Tomcat 11 on JDK 21: Real‑World JVM, Connection Pool, and Virtual Thread Tuning
This guide walks you through practical performance tuning of Tomcat 11 on JDK 21, covering JVM memory settings, GC selection, connector and thread‑pool optimization, virtual‑thread integration, benchmark scripts, security hardening, high‑availability deployment, and comprehensive monitoring and troubleshooting techniques.
Overview
Tomcat 11.x runs on Jakarta EE 11 and supports JDK 21 virtual threads (Project Loom). It can be tuned for high‑concurrency Java web services by adjusting JVM memory, GC, connector, thread pool, and database pool settings, then validating the changes with load‑testing tools such as wrk or JMeter.
Environment Requirements
OS: CentOS Stream 9, Ubuntu 22.04+, or Rocky Linux 9 (Linux 5.15+ kernel with io_uring support)
JDK: OpenJDK 21.0.4+ or Oracle JDK 21 (LTS, required for virtual threads)
Tomcat: 11.0.x (current 11.0.6)
Memory: ≥ 4 GB (recommended ≥ 8 GB) – allocate heap, metaspace, direct memory and leave OS reserve
CPU: ≥ 2 cores (recommended ≥ 4 cores) – GC thread count is based on cores
Load‑test tools: JMeter 5.6+ or wrk 4.2+
Preparation
System Check
# Verify OS version
cat /etc/os-release
# CPU cores
nproc
# Physical memory
free -h
# Disk space
df -h
# Kernel version
uname -rInstall JDK 21
# Ubuntu/Debian
sudo apt update && sudo apt install -y openjdk-21-jdk
# CentOS/Rocky
sudo dnf install -y java-21-openjdk java-21-openjdk-devel
# Verify
java -versionInstall Tomcat 11
# Download
cd /opt
wget https://dlcdn.apache.org/tomcat/tomcat-11/v11.0.6/bin/apache-tomcat-11.0.6.tar.gz
# Extract and link
tar -xzf apache-tomcat-11.0.6.tar.gz
ln -s /opt/apache-tomcat-11.0.6 /opt/tomcat
# Create dedicated user
useradd -r -s /sbin/nologin tomcat
chown -R tomcat:tomcat /opt/apache-tomcat-11.0.6
# Verify installation
/opt/tomcat/bin/version.shBaseline Benchmark
Run a quick load test before any tuning to capture QPS, latency, and error rate.
# Start Tomcat with default config
sudo -u tomcat /opt/tomcat/bin/startup.sh
# 30‑second wrk test (4 threads, 200 connections)
wrk -t4 -c200 -d30s http://localhost:8080/
# Stop Tomcat
/opt/tomcat/bin/shutdown.shCore Configuration
JVM Parameter Tuning (setenv.sh)
Create /opt/tomcat/bin/setenv.sh (executable) and add the following. Adjust values to match your hardware and workload.
#!/bin/bash
# Example for an 8 GB heap on an 8 GB+ server
CATALINA_OPTS="-Xms8g -Xmx8g \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
-XX:MaxDirectMemorySize=2g \
-XX:+UseG1GC -XX:MaxGCPauseMillis=150 -XX:G1HeapRegionSize=8m \
-XX:InitiatingHeapOccupancyPercent=45 -XX:ParallelGCThreads=6 -XX:ConcGCThreads=2 \
-XX:+UseStringDeduplication -XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs/heapdump.hprof \
-Xlog:gc*,gc+age=trace,gc+heap=debug:file=/opt/tomcat/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=50m"
export CATALINA_OPTSIf you need sub‑millisecond pause times or have a heap larger than 16 GB, replace the G1 block with ZGC:
# ZGC (generational by default in JDK 21)
CATALINA_OPTS="$CATALINA_OPTS -XX:+UseZGC -XX:SoftMaxHeapSize=3g -XX:ConcGCThreads=2"
export CATALINA_OPTSConnector Configuration (server.xml)
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
maxThreads="500"
minSpareThreads="50"
acceptCount="300"
connectionTimeout="20000"
maxConnections="10000"
maxKeepAliveRequests="200"
keepAliveTimeout="30000"
compression="on"
compressionMinSize="2048"
compressibleMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml"
URIEncoding="UTF-8"
server="WebServer"
maxHttpHeaderSize="16384"
maxPostSize="10485760" />Key points: protocol set to Http11Nio2Protocol gives 10‑20 % higher throughput under high concurrency. maxThreads should be 2‑4 × CPU cores for I/O‑bound services; maxConnections can be larger because a single NIO thread handles many connections.
Enable GZIP compression but keep compressionMinSize at 2048 B to avoid compressing tiny payloads.
Thread‑Pool (Executor) Configuration
<Executor name="tomcatThreadPool"
namePrefix="catalina-exec-"
maxThreads="500"
minSpareThreads="50"
maxIdleTime="60000"
prestartminSpareThreads="true"
maxQueueSize="200" />
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
executor="tomcatThreadPool"
... />Virtual‑Thread Executor (Tomcat 11 + JDK 21)
<Executor name="virtualThreadExecutor"
className="org.apache.catalina.core.StandardVirtualThreadExecutor"
namePrefix="vt-exec-" />
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
executor="virtualThreadExecutor"
maxConnections="20000"
connectionTimeout="20000"
acceptCount="500" />Virtual threads remove the need to size maxThreads. They are ideal for I/O‑heavy workloads (HTTP calls, DB queries, file I/O) but not for CPU‑bound computation.
Startup and Verification
# Ensure setenv.sh is executable
chmod +x /opt/tomcat/bin/setenv.sh
# Start Tomcat
sudo -u tomcat /opt/tomcat/bin/startup.sh
# Verify JVM flags
jcmd $(pgrep -f catalina) VM.flags
# Verify heap info
jcmd $(pgrep -f catalina) GC.heap_info
# Quick HTTP check (requires manager credentials)
curl -I -u admin:pwd http://localhost:8080/manager/statusPost‑Tuning Benchmark
# Same wrk parameters as baseline
wrk -t4 -c200 -d30s http://localhost:8080/
# Expected improvement: QPS +30‑100 %, P99 latency –40‑60 %Sample Configurations
Full setenv.sh Template (production)
#!/bin/bash
# Memory regions
HEAP_OPTS="-Xms8g -Xmx8g"
META_OPTS="-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
DIRECT_OPTS="-XX:MaxDirectMemorySize=2g"
# G1 GC (replace with ZGC block if needed)
GC_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=150 -XX:G1HeapRegionSize=16m -XX:InitiatingHeapOccupancyPercent=45 -XX:ParallelGCThreads=6 -XX:ConcGCThreads=2 -XX:+UseStringDeduplication"
# GC logging (JDK 21 unified format)
GC_LOG="-Xlog:gc*,gc+age=trace,gc+heap=debug:file=/opt/tomcat/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100m"
# OOM handling
OOM_OPTS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/tomcat/logs/heapdump-%t.hprof -XX:+ExitOnOutOfMemoryError"
# JMX (enable only if remote monitoring is required)
JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/opt/tomcat/conf/jmxremote.password -Dcom.sun.management.jmxremote.access.file=/opt/tomcat/conf/jmxremote.access"
# Miscellaneous
MISC_OPTS="-XX:+DisableExplicitGC -Djava.security.egd=file:/dev/./urandom -Dfile.encoding=UTF-8 -Djava.net.preferIPv4Stack=true"
# Combine all
export CATALINA_OPTS="$HEAP_OPTS $META_OPTS $DIRECT_OPTS $GC_OPTS $GC_LOG $OOM_OPTS $JMX_OPTS $MISC_OPTS"server.xml Core Snippet
<Server port="-1" shutdown="DISABLED">
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container" type="org.apache.catalina.UserDatabase" description="User database" factory="org.apache.catalina.users.MemoryUserDatabaseFactory" pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="500" minSpareThreads="50" maxIdleTime="60000" prestartminSpareThreads="true" maxQueueSize="200" />
<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" executor="tomcatThreadPool" maxConnections="10000" acceptCount="300" connectionTimeout="20000" maxKeepAliveRequests="200" keepAliveTimeout="30000" compression="on" compressionMinSize="2048" compressibleMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml" URIEncoding="UTF-8" server="WebServer" maxHttpHeaderSize="16384" maxPostSize="10485760" />
<Connector port="8443" protocol="org.apache.coyote.http11.Http11Nio2Protocol" executor="tomcatThreadPool" maxConnections="10000" SSLEnabled="true" scheme="https" secure="true">
<SSLHostConfig protocols="TLSv1.3,TLSv1.2" ciphers="TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256">
<Certificate certificateKeystoreFile="conf/keystore.p12" certificateKeystorePassword="changeit" type="RSA" />
</SSLHostConfig>
</Connector>
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="access_log" suffix=".log" pattern="%h %l %u %t \"%r\" %s %b %D" rotatable="true" maxDays="30" />
</Host>
</Engine>
</Service>
</Server>HikariCP DataSource (Spring Boot)
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://db-host:3306/mydb?useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: app_user
password: ${DB_PASSWORD}
hikari:
pool-name: HikariPool-Main
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
validation-timeout: 2000
leak-detection-threshold: 30000
connection-init-sql: SET NAMES utf8mb4Best Practices & Security Hardening
Performance Tips
Set -Xms and -Xmx to the same value to avoid runtime heap resizing.
Size thread pool based on workload: CPU‑bound ≈ cores + 1; I/O‑bound ≈ 2‑4 × cores. Use virtual threads for pure I/O workloads.
Prefer Http11Nio2Protocol over classic NIO for ~10‑20 % throughput gain.
Enable GZIP compression but keep compressionMinSize at 2048 B.
Pre‑warm HikariCP by setting minimum-idle equal to maximum-pool-size.
Disable autoDeploy in production to stop periodic WAR scanning.
Security Measures
Disable remote shutdown: set port="-1" shutdown="DISABLED" in server.xml.
Remove default webapps (ROOT, docs, examples, manager, host‑manager) from /opt/tomcat/webapps.
Hide Tomcat version by setting server="WebServer" and providing custom error pages in conf/web.xml.
If JMX is enabled, configure jmxremote.password and jmxremote.access with strict permissions.
Consider enabling the (deprecated) Java Security Manager only for legacy multi‑tenant scenarios; prefer container isolation for new deployments.
High Availability
Deploy at least two Tomcat instances behind a load balancer (NGINX, HAProxy).
Externalize HTTP session state to Redis (e.g., RedisSessionHandlerValve) or Spring Session.
Use graceful shutdown ( /opt/tomcat/bin/shutdown.sh 30) to allow in‑flight requests to finish.
Version‑control the conf/ directory with Git and back up webapps/ regularly.
Troubleshooting & Monitoring
Log Inspection
# Core logs
tail -f /opt/tomcat/logs/catalina.out
# Date‑rotated catalina log
tail -f /opt/tomcat/logs/catalina.$(date +%Y-%m-%d).log
# Access log (contains response time)
tail -f /opt/tomcat/logs/access_log.$(date +%Y-%m-%d).log
# GC log
tail -f /opt/tomcat/logs/gc.log
# Search for errors
grep -i "ERROR\|SEVERE\|Exception\|OutOfMemory" /opt/tomcat/logs/catalina.out | tail -50
# Find slow requests (>5 s)
awk -F'"' '{print $0}' /opt/tomcat/logs/access_log.*.log | awk '{if($NF>5000) print $0}' | tail -20Common Issues
java.lang.OutOfMemoryError: Java heap space– increase -Xmx or analyze heap dump with MAT. java.lang.OutOfMemoryError: Metaspace – increase -XX:MaxMetaspaceSize and avoid hot‑deploy in production. java.lang.OutOfMemoryError: Direct buffer memory – increase -XX:MaxDirectMemorySize or close unused ByteBuffer s. TaskQueue: maxThreads reached – raise maxThreads or fix slow requests. Connection pool exhausted – enlarge HikariCP pool, fix slow SQL, enable leak detection. Too many open files – raise ulimit -n (e.g., 65535).
GC Log Quick Analysis (bash)
#!/bin/bash
GC_LOG=${1:-/opt/tomcat/logs/gc.log}
if [ ! -f "$GC_LOG" ]; then
echo "GC log not found: $GC_LOG"
exit 1
fi
echo "===== GC LOG SUMMARY ====="
# Count events
echo "Young GC count: $(grep -c 'Pause Young' "$GC_LOG")"
echo "Full GC count: $(grep -c 'Pause Full' "$GC_LOG")"
# Average/Max Young pause
grep -oP 'Pause Young.*?([0-9]+\.[0-9]+)ms' "$GC_LOG" | grep -oP '[0-9]+\.[0-9]+' |
awk '{sum+=$1; cnt++; if($1>max) max=$1} END {print "Young GC avg pause: " sum/cnt " ms, max: " max " ms"}'
# Warn on Full GC
FULL=$(grep -c 'Pause Full' "$GC_LOG")
if [ $FULL -gt 0 ]; then
echo "!!! Detected $FULL Full GC events !!!"
tail -3 "$GC_LOG" | grep 'Pause Full'
fi
echo "===== END OF SUMMARY ====="Prometheus + Grafana Monitoring (JMX Exporter)
Download the JMX Exporter agent and add it to setenv.sh:
wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/1.0.1/jmx_prometheus_javaagent-1.0.1.jar -O /opt/tomcat/lib/jmx_exporter.jar
export CATALINA_OPTS="$CATALINA_OPTS -javaagent:/opt/tomcat/lib/jmx_exporter.jar=9404:/opt/tomcat/conf/jmx_exporter_config.yaml"Sample jmx_exporter_config.yaml (kept as plain text):
startDelaySeconds: 0
lowercaseOutputName: true
lowercaseOutputLabelNames: true
rules:
- pattern: 'Catalina<type=ThreadPool, name="(.*)">(.*)'
name: tomcat_threadpool_$2
labels:
connector: "$1"
type: GAUGE
- pattern: 'Catalina<type=GlobalRequestProcessor, name="(.*)">(.*)'
name: tomcat_request_$2
labels:
connector: "$1"
type: COUNTER
- pattern: 'Catalina<type=Manager, host=(.*), context=(.*)><>activeSessions'
name: tomcat_sessions_active
labels:
host: "$1"
context: "$2"
type: GAUGE
- pattern: 'java.lang<type=Memory><HeapMemoryUsage>(.*)'
name: jvm_heap_memory_$1
type: GAUGE
- pattern: 'com.zaxxer.hikari<type=Pool \((.*)\)>(.*)'
name: hikaricp_$2
labels:
pool: "$1"
type: GAUGEPrometheus scrape config snippet (plain text):
scrape_configs:
- job_name: 'tomcat'
scrape_interval: 15s
static_configs:
- targets: ['tomcat-host:9404']
labels:
instance: 'tomcat-prod-01'Backup & Restore
Backup Script
#!/bin/bash
TOMCAT_HOME="/opt/tomcat"
BACKUP_DIR="/data/backup/tomcat"
DATE=$(date +%Y%m%d_%H%M%S)
RETAIN_DAYS=30
mkdir -p "$BACKUP_DIR"
# Config backup
tar -czf "$BACKUP_DIR/tomcat_conf_${DATE}.tar.gz" -C "$TOMCAT_HOME" conf/
# Webapps backup
tar -czf "$BACKUP_DIR/tomcat_webapps_${DATE}.tar.gz" -C "$TOMCAT_HOME" webapps/
# setenv.sh backup
cp "$TOMCAT_HOME/bin/setenv.sh" "$BACKUP_DIR/setenv_${DATE}.sh"
# systemd service backup (optional)
cp /etc/systemd/system/tomcat.service "$BACKUP_DIR/tomcat_service_${DATE}" 2>/dev/null
# Clean old backups
find "$BACKUP_DIR" -type f -mtime +$RETAIN_DAYS -delete
echo "[$(date)] Backup completed: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"/*${DATE}*Restore Procedure
# Stop service
sudo systemctl stop tomcat
# Backup current config (just in case)
cp -r /opt/tomcat/conf /opt/tomcat/conf.bak.$(date +%s)
# Restore config
tar -xzf /data/backup/tomcat/tomcat_conf_20260225_100000.tar.gz -C /opt/tomcat/
# Restore webapps (if needed)
tar -xzf /data/backup/tomcat/tomcat_webapps_20260225_100000.tar.gz -C /opt/tomcat/
# Fix permissions
chown -R tomcat:tomcat /opt/tomcat/conf /opt/tomcat/webapps
# Verify syntax (optional)
/opt/tomcat/bin/configtest.sh
# Start service and health check
sudo systemctl start tomcat
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/
# Expected 200Conclusion
Key Takeaways
Align -Xms and -Xmx to lock heap size and avoid runtime resizing.
Select GC based on heap size: G1 for typical workloads, ZGC for ultra‑low latency or > 16 GB heaps.
Connector and thread‑pool tuning (NIO2, maxThreads, maxConnections) directly affect throughput; virtual threads eliminate manual sizing for I/O‑bound services.
Size HikariCP pool using the formula (CPU × 2) + disk and keep minimum-idle equal to maximum-pool-size to avoid runtime scaling overhead.
Enable structured GC logging ( -Xlog:gc*) and use tools like GCEasy or GCViewer for quick analysis.
Always benchmark before and after tuning; use wrk for quick tests and JMeter for full‑scenario validation.
Further Learning
Deep dive into JVM performance engineering with Java Flight Recorder (JFR) and Java Mission Control (JMC).
Container‑aware tuning: -XX:ActiveProcessorCount, -XX:MaxRAMPercentage, and cgroup memory limits.
Explore GraalVM native images for ultra‑fast startup and low memory footprints in micro‑service environments.
References
Apache Tomcat 11 Official Documentation
Oracle JDK 21 GC Tuning Guide
HikariCP GitHub Repository
Prometheus JMX Exporter GitHub
GCEasy Online GC Log Analyzer
JDK 21 Release Notes
Ops Community
A leading IT operations community where professionals share and grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
