Java Performance Tuning: Complete Guide

1️⃣ Introduction

Performance tuning is crucial for optimizing Java applications. This comprehensive guide covers essential techniques, tools, and best practices for improving Java application performance.

Key areas covered:

  • Memory Management and Optimization
  • JVM Tuning and Configuration
  • Code-Level Optimizations
  • Database and I/O Performance
  • Concurrency Optimization
  • Profiling and Monitoring
  • Performance Testing
  • Common Performance Issues

2️⃣ Memory Management

🔹 Memory Leak Detection

// Using WeakHashMap to prevent memory leaks
public class CacheManager {
    private final WeakHashMap cache = 
        new WeakHashMap<>();
    
    public void cacheData(String key, byte[] data) {
        cache.put(key, data);
    }
    
    // Using try-with-resources for proper resource cleanup
    public void processLargeFile(String filePath) {
        try (FileInputStream fis = new FileInputStream(filePath);
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            // Process file
        } catch (IOException e) {
            logger.error("Error processing file: " + e.getMessage());
        }
    }
}

// Memory leak prevention in custom collections
public class CustomCache {
    private final int maxSize;
    private final Map cache;
    
    public CustomCache(int maxSize) {
        this.maxSize = maxSize;
        this.cache = new LinkedHashMap(maxSize, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > maxSize;
            }
        };
    }
}

🔹 Object Pool Implementation

public class ObjectPool {
    private final Queue pool;
    private final Supplier factory;
    private final int maxSize;
    
    public ObjectPool(Supplier factory, int maxSize) {
        this.factory = factory;
        this.maxSize = maxSize;
        this.pool = new ConcurrentLinkedQueue<>();
    }
    
    public T borrow() {
        T instance = pool.poll();
        return instance != null ? instance : factory.get();
    }
    
    public void release(T instance) {
        if (pool.size() < maxSize) {
            pool.offer(instance);
        }
    }
    
    // Usage example
    public static void main(String[] args) {
        ObjectPool pool = 
            new ObjectPool<>(StringBuilder::new, 100);
        
        StringBuilder sb = pool.borrow();
        try {
            sb.append("Hello").append(" World");
            // Use StringBuilder
        } finally {
            sb.setLength(0); // Reset
            pool.release(sb);
        }
    }
}

3️⃣ JVM Tuning

🔹 Garbage Collection Configuration

# G1GC Configuration
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16M
-XX:G1ReservePercent=10
-XX:InitiatingHeapOccupancyPercent=45

# Memory Settings
-Xms4g
-Xmx4g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# GC Logging
-Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=100m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps

# Thread Stack Size
-Xss512k

# Large Pages
-XX:+UseLargePages
-XX:LargePageSizeInBytes=2m

🔹 Performance Monitoring

// JMX Monitoring Configuration
@Configuration
public class JmxConfig {
    @Bean
    public MBeanExporter mBeanExporter() {
        MBeanExporter exporter = new MBeanExporter();
        Map beans = new HashMap<>();
        beans.put("bean:name=performanceMonitor", 
            new PerformanceMonitorMBean());
        exporter.setBeans(beans);
        return exporter;
    }
}

// Custom Performance MBean
public class PerformanceMonitorMBean implements PerformanceMonitorMXBean {
    private final Map timers = new ConcurrentHashMap<>();
    
    @Override
    public void startTimer(String name) {
        timers.computeIfAbsent(name, k -> new Timer()).start();
    }
    
    @Override
    public long stopTimer(String name) {
        Timer timer = timers.get(name);
        return timer != null ? timer.stop() : -1;
    }
    
    @Override
    public Map getTimings() {
        return timers.entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> e.getValue().getElapsedTime()
            ));
    }
}

4️⃣ Code-Level Optimizations

🔹 String Operations

// String concatenation optimization
public class StringOptimization {
    // Bad practice
    public String concatenateStrings(List strings) {
        String result = "";
        for (String s : strings) {
            result += s; // Creates new String object each time
        }
        return result;
    }
    
    // Good practice
    public String concatenateStringsOptimized(List strings) {
        StringBuilder sb = new StringBuilder(strings.size() * 16);
        for (String s : strings) {
            sb.append(s);
        }
        return sb.toString();
    }
    
    // String interning
    public void stringInternExample() {
        String s1 = new String("hello").intern();
        String s2 = "hello";
        assert s1 == s2; // true after interning
    }
    
    // String formatting
    public String formatString(String name, int age) {
        // Bad practice
        return "Name: " + name + ", Age: " + age;
        
        // Good practice
        return String.format("Name: %s, Age: %d", name, age);
        
        // Better practice for frequent use
        return new StringBuilder(32)
            .append("Name: ")
            .append(name)
            .append(", Age: ")
            .append(age)
            .toString();
    }
}

🔹 Collection Optimization

public class CollectionOptimization {
    // Initialize with proper size
    public List createList(int size) {
        return new ArrayList<>(size); // Prevents resizing
    }
    
    // Use proper collection type
    public Set createSet(boolean ordered) {
        return ordered ? new LinkedHashSet<>() : new HashSet<>();
    }
    
    // Bulk operations
    public void bulkOperations(List source) {
        List destination = new ArrayList<>(source.size());
        destination.addAll(source); // Better than individual adds
        
        // Use removeIf instead of iterator
        destination.removeIf(s -> s.isEmpty());
    }
    
    // Custom sorting
    public void sortList(List people) {
        // Create comparator once
        Comparator comparator = Comparator
            .comparing(Person::getLastName)
            .thenComparing(Person::getFirstName);
            
        people.sort(comparator);
    }
    
    // Use streams efficiently
    public List processStrings(List input) {
        return input.stream()
            .filter(s -> !s.isEmpty())
            .map(String::trim)
            .distinct()
            .collect(Collectors.toCollection(
                () -> new ArrayList<>(input.size())
            ));
    }
}

5️⃣ Database and I/O Performance

🔹 Connection Pooling

// HikariCP Configuration
@Configuration
public class DatabaseConfig {
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
        config.setUsername("user");
        config.setPassword("password");
        
        // Pool settings
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(5);
        config.setIdleTimeout(300000);
        config.setConnectionTimeout(20000);
        config.setMaxLifetime(1200000);
        
        // Performance settings
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        
        return new HikariDataSource(config);
    }
}

🔹 Batch Processing

@Service
@Transactional
public class BatchProcessor {
    private final JdbcTemplate jdbcTemplate;
    private final int batchSize = 1000;
    
    public void batchInsert(List people) {
        String sql = "INSERT INTO person (first_name, last_name, age) " +
                    "VALUES (?, ?, ?)";
                    
        jdbcTemplate.batchUpdate(sql, 
            new BatchPreparedStatementSetter() {
                @Override
                public void setValues(PreparedStatement ps, int i) 
                        throws SQLException {
                    Person person = people.get(i);
                    ps.setString(1, person.getFirstName());
                    ps.setString(2, person.getLastName());
                    ps.setInt(3, person.getAge());
                }
                
                @Override
                public int getBatchSize() {
                    return people.size();
                }
            });
    }
    
    public void processLargeDataset(Stream people) {
        List batch = new ArrayList<>(batchSize);
        
        people.forEach(person -> {
            batch.add(person);
            if (batch.size() >= batchSize) {
                batchInsert(batch);
                batch.clear();
            }
        });
        
        if (!batch.isEmpty()) {
            batchInsert(batch);
        }
    }
}

6️⃣ Concurrency Optimization

🔹 Thread Pool Configuration

@Configuration
public class ThreadPoolConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        
        // Core pool size
        executor.setCorePoolSize(Runtime.getRuntime()
            .availableProcessors());
        
        // Maximum pool size
        executor.setMaxPoolSize(Runtime.getRuntime()
            .availableProcessors() * 2);
        
        // Queue capacity
        executor.setQueueCapacity(500);
        
        // Thread naming
        executor.setThreadNamePrefix("async-");
        
        // Rejection policy
        executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.CallerRunsPolicy());
        
        executor.initialize();
        return executor;
    }
}

@Service
public class AsyncService {
    private final Executor executor;
    
    public CompletableFuture processAsync(Request request) {
        return CompletableFuture.supplyAsync(() -> {
            // Process request
            return new Result();
        }, executor);
    }
    
    public void processBatch(List requests) {
        List> futures = requests.stream()
            .map(this::processAsync)
            .collect(Collectors.toList());
            
        CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0]))
            .join();
    }
}

🔹 Lock Optimization

public class LockOptimization {
    // Use ReentrantLock for complex scenarios
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    
    public void complexOperation() {
        lock.lock();
        try {
            // Critical section
            while (!readyToProcess()) {
                condition.await();
            }
            process();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
    
    // Use ReadWriteLock for read-heavy scenarios
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    
    public void readOperation() {
        readLock.lock();
        try {
            // Read operation
        } finally {
            readLock.unlock();
        }
    }
    
    public void writeOperation() {
        writeLock.lock();
        try {
            // Write operation
        } finally {
            writeLock.unlock();
        }
    }
}

7️⃣ Profiling and Monitoring

🔹 JFR Configuration

# JFR Recording Options
-XX:StartFlightRecording=duration=60s,filename=recording.jfr,
settings=profile

# Custom JFR Event
@Label("Custom Performance Event")
@Name("com.example.CustomEvent")
@Category("Performance")
@Description("Custom performance monitoring event")
public class CustomEvent extends Event {
    @Label("Operation Name")
    private final String operation;
    
    @Label("Duration (ms)")
    private final long duration;
    
    public CustomEvent(String operation, long duration) {
        this.operation = operation;
        this.duration = duration;
    }
    
    public void record() {
        if (isEnabled()) {
            commit();
        }
    }
}

🔹 Metrics Collection

@Configuration
public class MetricsConfig {
    @Bean
    public MeterRegistry meterRegistry() {
        return new SimpleMeterRegistry();
    }
}

@Service
public class PerformanceMonitor {
    private final MeterRegistry registry;
    private final Map timers = new ConcurrentHashMap<>();
    
    public void recordOperation(String name, Runnable operation) {
        Timer timer = timers.computeIfAbsent(name,
            k -> Timer.builder(k)
                .description("Operation timing")
                .register(registry));
                
        timer.record(operation);
    }
    
    public void recordValue(String name, double value) {
        Gauge.builder(name, () -> value)
            .description("Custom metric")
            .register(registry);
    }
    
    public void countEvent(String name) {
        Counter counter = Counter.builder(name)
            .description("Event counter")
            .register(registry);
        counter.increment();
    }
}

8️⃣ Q&A / Frequently Asked Questions

To identify performance bottlenecks: (1) Use profiling tools like JFR or YourKit. (2) Monitor CPU, memory, and I/O usage. (3) Analyze thread dumps. (4) Use metrics and logging. (5) Perform load testing. (6) Monitor GC behavior. (7) Use APM tools. (8) Analyze database query performance. (9) Monitor network latency. (10) Review application logs.

Memory management best practices: (1) Use appropriate collection types. (2) Avoid memory leaks. (3) Implement proper object pooling. (4) Use weak references when appropriate. (5) Size collections properly. (6) Clean up resources properly. (7) Monitor heap usage. (8) Use proper GC settings. (9) Implement caching strategies. (10) Regular memory profiling.

Database optimization strategies: (1) Use connection pooling. (2) Implement proper indexing. (3) Optimize queries. (4) Use batch processing. (5) Implement caching. (6) Monitor query performance. (7) Use prepared statements. (8) Optimize table design. (9) Regular maintenance. (10) Use appropriate transaction isolation levels.

9️⃣ Best Practices & Pro Tips 🚀

  • Profile before optimizing
  • Use appropriate data structures
  • Implement proper caching
  • Optimize database access
  • Monitor application metrics
  • Use proper GC settings
  • Implement connection pooling
  • Optimize I/O operations
  • Use batch processing
  • Regular performance testing
  • Monitor memory usage
  • Document optimization strategies

Read Next 📖

Conclusion

Performance tuning is an essential aspect of Java application development. By following the techniques and best practices outlined in this guide, you can significantly improve your application's performance and resource utilization.

Remember to always measure and profile before optimizing, and focus on the areas that will provide the most significant performance improvements for your specific use case.