Performance Tuning
1. JVM Tuning
Memory Configuration
# Heap Size Configuration
-Xms2g # Initial Heap Size
-Xmx4g # Maximum Heap Size
-XX:MetaspaceSize=256m # Initial Metaspace Size
-XX:MaxMetaspaceSize=512m # Maximum Metaspace Size
# Garbage Collection
-XX:+UseG1GC # Use G1 Garbage Collector
-XX:MaxGCPauseMillis=200 # Target GC Pause Time
-XX:ParallelGCThreads=4 # Number of GC Threads
-XX:ConcGCThreads=2 # Number of Concurrent GC Threads
# Memory Management
-XX:+HeapDumpOnOutOfMemoryError # Create Heap Dump on OOM
-XX:HeapDumpPath=/var/log/heap-dump.hprof # Heap Dump Location
JVM Monitoring
@Configuration
public class JvmMonitoringConfig {
@Bean
public MeterRegistry jvmMetrics() {
return new JvmMetricsRegistry();
}
@Bean
public JvmGcMetrics gcMetrics() {
return new JvmGcMetrics();
}
@Bean
public JvmMemoryMetrics memoryMetrics() {
return new JvmMemoryMetrics();
}
@Bean
public JvmThreadMetrics threadMetrics() {
return new JvmThreadMetrics();
}
}
@Component
public class JvmHealthIndicator implements HealthIndicator {
private final Runtime runtime = Runtime.getRuntime();
@Override
public Health health() {
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long maxMemory = runtime.maxMemory();
return Health.up()
.withDetail("total_memory", formatSize(totalMemory))
.withDetail("free_memory", formatSize(freeMemory))
.withDetail("max_memory", formatSize(maxMemory))
.withDetail("processors", runtime.availableProcessors())
.build();
}
private String formatSize(long bytes) {
return bytes / (1024 * 1024) + "MB";
}
}
2. Thread Pool Optimization
Executor Configuration
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor applicationTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(120);
executor.setThreadNamePrefix("app-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// Monitor thread pool metrics
executor.setThreadFactory(new MonitoredThreadFactory());
return executor;
}
}
public class MonitoredThreadFactory implements ThreadFactory {
private final ThreadFactory delegate = Executors.defaultThreadFactory();
private final MeterRegistry registry;
public Thread newThread(Runnable r) {
Thread thread = delegate.newThread(r);
registry.gauge("thread.pool.active",
Tags.of("name", thread.getName()),
thread,
t -> t.isAlive() ? 1 : 0);
return thread;
}
}
Async Processing
@Service
public class AsyncProcessingService {
private final ThreadPoolTaskExecutor executor;
private final MeterRegistry registry;
@Async
public CompletableFuture<ProcessingResult> processAsync(Task task) {
Timer.Sample sample = Timer.start(registry);
try {
ProcessingResult result = processTask(task);
sample.stop(Timer.builder("async.processing")
.tag("task", task.getType())
.register(registry));
return CompletableFuture.completedFuture(result);
} catch (Exception e) {
registry.counter("async.errors",
"task", task.getType()).increment();
throw e;
}
}
@Scheduled(fixedRate = 60000)
public void monitorThreadPool() {
ThreadPoolExecutor threadPool = executor.getThreadPoolExecutor();
registry.gauge("thread.pool.size",
threadPool.getPoolSize());
registry.gauge("thread.pool.active",
threadPool.getActiveCount());
registry.gauge("thread.pool.queue",
threadPool.getQueue().size());
}
}
3. Database Connection Pool
HikariCP Optimization
@Configuration
public class DatabaseConfig {
@Bean
public HikariConfig hikariConfig() {
HikariConfig config = new HikariConfig();
// Basic Configuration
config.setDriverClassName("org.postgresql.Driver");
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("password");
// Pool Configuration
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setIdleTimeout(300000); // 5 minutes
config.setMaxLifetime(1800000); // 30 minutes
config.setConnectionTimeout(30000); // 30 seconds
// Performance Optimization
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
config.addDataSourceProperty("useServerPrepStmts", "true");
return config;
}
@Bean
public DataSource dataSource() {
return new HikariDataSource(hikariConfig());
}
}
Connection Pool Monitoring
@Component
public class ConnectionPoolMetrics {
private final HikariDataSource dataSource;
private final MeterRegistry registry;
@Scheduled(fixedRate = 10000)
public void recordMetrics() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
registry.gauge("hikari.connections.active",
poolMXBean.getActiveConnections());
registry.gauge("hikari.connections.idle",
poolMXBean.getIdleConnections());
registry.gauge("hikari.connections.total",
poolMXBean.getTotalConnections());
registry.gauge("hikari.connections.pending",
poolMXBean.getThreadsAwaitingConnection());
}
@EventListener
public void onPoolStatsChange(PoolStatsEvent event) {
logger.info("Connection pool stats: active={}, idle={}, waiting={}",
event.getActiveConnections(),
event.getIdleConnections(),
event.getThreadsAwaitingConnection());
}
}
4. Caching Strategy
Multi-Level Cache
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new LayeredCacheManager(
localCache(),
redisCache()
);
}
@Bean
public CacheManager localCache() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.recordStats());
return cacheManager;
}
@Bean
public CacheManager redisCache() {
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())))
.build();
return cacheManager;
}
}
Cache Monitoring
@Component
public class CacheMonitor {
private final MeterRegistry registry;
private final CacheManager cacheManager;
@Scheduled(fixedRate = 60000)
public void recordMetrics() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
CacheStats stats = ((CaffeineCache) cache).getNativeCache().stats();
registry.gauge("cache.size",
Tags.of("name", cacheName),
cache,
this::getCacheSize);
registry.gauge("cache.hit.ratio",
Tags.of("name", cacheName),
stats.hitRate());
registry.gauge("cache.miss.ratio",
Tags.of("name", cacheName),
stats.missRate());
});
}
private long getCacheSize(Cache cache) {
if (cache instanceof CaffeineCache) {
return ((CaffeineCache) cache).getNativeCache().estimatedSize();
}
return 0;
}
}
5. Best Practices
1. Resource Management
@Component
public class ResourceManager {
private final ExecutorService executor;
private final DataSource dataSource;
@PreDestroy
public void cleanup() {
// Graceful shutdown of thread pools
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
// Close database connections
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
}
}
public void monitorResources() {
// Monitor thread usage
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadBean.getAllThreadIds();
registry.gauge("threads.total", threadIds.length);
registry.gauge("threads.blocked",
Arrays.stream(threadBean.getThreadInfo(threadIds))
.filter(info -> info.getThreadState() == Thread.State.BLOCKED)
.count());
// Monitor memory usage
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
registry.gauge("memory.used", heapUsage.getUsed());
registry.gauge("memory.committed", heapUsage.getCommitted());
registry.gauge("memory.max", heapUsage.getMax());
}
}
2. Response Time Optimization
@Aspect
@Component
public class PerformanceMonitor {
private final MeterRegistry registry;
private final Logger logger = LoggerFactory.getLogger(PerformanceMonitor.class);
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object monitorEndpoint(ProceedingJoinPoint joinPoint) throws Throwable {
Timer.Sample sample = Timer.start(registry);
String endpoint = joinPoint.getSignature().toShortString();
try {
Object result = joinPoint.proceed();
sample.stop(Timer.builder("http.server.requests")
.tag("endpoint", endpoint)
.register(registry));
return result;
} catch (Exception e) {
registry.counter("http.server.errors",
"endpoint", endpoint,
"error", e.getClass().getSimpleName()
).increment();
throw e;
}
}
@Scheduled(fixedRate = 60000)
public void checkSlowEndpoints() {
registry.find("http.server.requests")
.timer()
.ifPresent(timer -> {
if (timer.mean(TimeUnit.MILLISECONDS) > 1000) {
logger.warn("Slow endpoint detected: {}",
timer.getId().getTag("endpoint"));
}
});
}
}
3. Load Testing
@SpringBootTest
public class PerformanceTest {
@Autowired
private WebTestClient webClient;
@Test
public void loadTest() {
int users = 100;
int requestsPerUser = 50;
Flux.range(1, users)
.flatMap(user ->
Flux.range(1, requestsPerUser)
.flatMap(request ->
webClient.get()
.uri("/api/products")
.exchange()
.doOnNext(response ->
assertTrue(response.statusCode().is2xxSuccessful()))
)
)
.blockLast(Duration.ofMinutes(5));
}
@Test
public void stressTest() {
StressTestBuilder.builder()
.withConcurrentUsers(100)
.withRampUpPeriod(Duration.ofSeconds(30))
.withHoldPeriod(Duration.ofMinutes(5))
.withTestCase(() ->
webClient.get()
.uri("/api/products")
.exchange()
.block()
)
.withAssertions(results -> {
assertTrue(results.getErrorRate() < 0.01);
assertTrue(results.getP95ResponseTime()
.compareTo(Duration.ofSeconds(1)) < 0);
})
.build()
.execute();
}
}
4. Memory Optimization
@Service
public class MemoryOptimizationService {
private final LoadingCache<String, byte[]> cache;
public MemoryOptimizationService() {
this.cache = Caffeine.newBuilder()
.maximumWeight(100_000_000) // 100MB
.weigher((String key, byte[] value) -> value.length)
.build(this::loadData);
}
public void processLargeData(InputStream input) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(input))) {
String line;
StringBuilder buffer = new StringBuilder(8192);
while ((line = reader.readLine()) != null) {
if (buffer.length() + line.length() > 8192) {
processBuffer(buffer);
buffer.setLength(0);
}
buffer.append(line);
}
if (buffer.length() > 0) {
processBuffer(buffer);
}
}
}
@Scheduled(fixedRate = 3600000)
public void cleanupMemory() {
System.gc();
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
logger.info("Memory usage: total={}, free={}, used={}",
formatSize(totalMemory),
formatSize(freeMemory),
formatSize(usedMemory));
}
}
5. Monitoring and Alerting
```java @Configuration public class MonitoringConfig {
@Bean
public MeterRegistry meterRegistry() {
CompositeMeterRegistry registry = new CompositeMeterRegistry();
registry.add(new SimpleMeterRegistry());
registry.add(new PrometheusRegistry());
registry.config()
.commonTags("application", "myapp")
.meterFilter(MeterFilter.deny(id ->
id.getName().startsWith("jvm")));
return registry;
}
}
@Component public class AlertingService {
private final MeterRegistry registry;
private final AlertNotifier notifier;
@Scheduled(fixedRate = 60000)
public void checkMetrics() {
// Check CPU usage
registry.find("system.cpu.usage")
.gauge()
.ifPresent(gauge -> {
if (gauge.value() > 0.8) { // 80%
notifier.sendAlert(
"High CPU Usage",
String.format("CPU usage is at %.2f%%",
gauge.value() * 100));
}
});
// Check memory usage
registry.find("jvm.memory.used")
.gauge()
.ifPresent(gauge -> {
double usedMemory = gauge.value() / (1024 * 1024); // MB
if (usedMemory > 1000) { // 1GB
notifier.sendAlert(
"High Memory Usage",
String.format("Memory usage is at %.2f MB",
usedMemory));
}
});
// Check error rate
registry.find("http.server.errors")
.counter()
.ifPresent(counter -> {
double errorRate = counter.count() /
registry.find("http.server.requests")
.timer()
.map(Timer::count)
.orElse(1.0);
if (errorRate > 0.05) { // 5%
notifier.sendAlert(
"High Error Rate",
String.format("Error rate is at %.2f%%",
errorRate * 100));
}
});
}
}