Performance optimization is crucial for building scalable and efficient Java applications. This comprehensive guide covers essential techniques, tools, and best practices for improving Java application performance, from JVM tuning to code-level optimizations.
Key areas of performance optimization:
The JVM manages different memory areas:
// Memory leak through static collection
public class CacheManager {
// Bad: Unbounded static collection
private static final Map<String, Object> cache = new HashMap<>();
public static void addToCache(String key, Object value) {
cache.put(key, value); // Memory leak: never removed
}
}
// Better: Using WeakHashMap or cache with eviction
public class ImprovedCacheManager {
private static final Map<String, Object> cache =
Collections.synchronizedMap(new WeakHashMap<>());
// Or using Caffeine cache with eviction
private static final Cache<String, Object> caffeine = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
}
// Poor resource management
public void processFile(String path) {
FileInputStream fis = null;
try {
fis = new FileInputStream(path);
// Process file
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// Better resource management with try-with-resources
public void processFile(String path) {
try (FileInputStream fis = new FileInputStream(path);
BufferedInputStream bis = new BufferedInputStream(fis)) {
// Process file
} catch (IOException e) {
log.error("Error processing file: {}", path, e);
throw new FileProcessingException(e);
}
}
// Common GC tuning parameters
java -XX:+UseG1GC \ # Use G1 Garbage Collector
-Xms4g \ # Initial heap size
-Xmx4g \ # Maximum heap size
-XX:MaxGCPauseMillis=200 \ # Target GC pause time
-XX:ParallelGCThreads=4 \ # Number of GC threads
-XX:ConcGCThreads=2 \ # Number of concurrent GC threads
-XX:InitiatingHeapOccupancyPercent=45 \ # Start GC at 45% heap usage
-Xlog:gc*:file=gc.log \ # GC logging
MyApplication
// ZGC for low latency applications
java -XX:+UseZGC \
-Xms8g \
-Xmx8g \
-XX:ZCollectionInterval=5 \
MyApplication
Key memory-related JVM options:
-Xms
: Initial heap size-Xmx
: Maximum heap size-XX:MaxMetaspaceSize
: Maximum metaspace size-XX:MaxDirectMemorySize
: Maximum direct memory size-XX:ReservedCodeCacheSize
: Maximum code cache size// JIT compilation settings
java -XX:+PrintCompilation \ # Print JIT compilation details
-XX:CompileThreshold=10000 \ # Method invocation threshold for compilation
-XX:+UseCodeCacheFlushing \ # Enable code cache flushing
-XX:ReservedCodeCacheSize=256m \ # Code cache size
MyApplication
// Profile-guided optimization
java -XX:+UseProfiledJIT \
-XX:CompileCommand=print,*ClassName.method \
MyApplication
// Inefficient string concatenation
String result = "";
for (int i = 0; i < items.length; i++) {
result += items[i]; // Creates many temporary objects
}
// Optimized using StringBuilder
StringBuilder result = new StringBuilder(items.length * 16); // Pre-sized
for (int i = 0; i < items.length; i++) {
result.append(items[i]);
}
String finalResult = result.toString();
// Using StringJoiner for delimiter-separated strings
StringJoiner joiner = new StringJoiner(", ");
for (String item : items) {
joiner.add(item);
}
String finalResult = joiner.toString();
// Initialize with proper capacity
Map<String, User> userMap = new HashMap<>(expectedSize);
List<Order> orders = new ArrayList<>(initialCapacity);
// Use appropriate collection types
// For frequent lookups
Set<String> uniqueIds = new HashSet<>();
// For ordered access
NavigableMap<LocalDateTime, Event> timeline = new TreeMap<>();
// For concurrent access
Map<String, User> userCache = new ConcurrentHashMap<>();
Queue<Task> taskQueue = new ConcurrentLinkedQueue<>();
// Bulk operations
List<Order> newOrders = Arrays.asList(order1, order2, order3);
orders.addAll(newOrders); // Better than individual adds
// Inefficient stream usage
List<User> activeUsers = users.stream()
.filter(User::isActive)
.collect(Collectors.toList());
activeUsers.stream()
.filter(user -> user.getAge() > 18)
.collect(Collectors.toList());
// Optimized stream operations
List<User> activeAdultUsers = users.stream()
.filter(User::isActive)
.filter(user -> user.getAge() > 18)
.collect(Collectors.toList());
// Parallel streams for CPU-intensive operations
List<Integer> processedData = data.parallelStream()
.map(this::heavyComputation)
.collect(Collectors.toList());
// 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");
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
config.setIdleTimeout(300000);
config.setConnectionTimeout(20000);
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
return new HikariDataSource(config);
}
}
// Batch inserts with JDBC
public void batchInsert(List<User> users) {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
conn.setAutoCommit(false);
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
pstmt.setString(1, user.getName());
pstmt.setString(2, user.getEmail());
pstmt.addBatch();
if ((i + 1) % 1000 == 0) {
pstmt.executeBatch();
}
}
pstmt.executeBatch();
conn.commit();
}
}
// JPA batch processing
@Configuration
public class JpaConfig {
@Bean
public Properties hibernateProperties() {
Properties props = new Properties();
props.setProperty("hibernate.jdbc.batch_size", "50");
props.setProperty("hibernate.order_inserts", "true");
props.setProperty("hibernate.order_updates", "true");
props.setProperty("hibernate.batch_versioned_data", "true");
return props;
}
}
// Using Caffeine cache
@Configuration
public class CacheConfig {
@Bean
public Cache<String, User> userCache() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats()
.build();
}
}
// Spring Cache abstraction
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUser(String id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
}
// Custom thread pool configuration
@Configuration
public class ThreadPoolConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncTask-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
// Using CompletableFuture for async operations
@Service
public class OrderService {
@Async
public CompletableFuture<OrderResult> processOrder(Order order) {
return CompletableFuture.supplyAsync(() -> {
// Process order
return new OrderResult();
}, taskExecutor);
}
public void processOrders(List<Order> orders) {
List<CompletableFuture<OrderResult>> futures = orders.stream()
.map(this::processOrder)
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.join();
}
}
// Using ReentrantLock for fine-grained locking
public class OptimizedCache<K, V> {
private final Map<K, V> cache = new ConcurrentHashMap<>();
private final ReentrantLock lock = new ReentrantLock();
public V get(K key, Supplier<V> loader) {
V value = cache.get(key);
if (value == null) {
if (lock.tryLock()) {
try {
value = cache.get(key);
if (value == null) {
value = loader.get();
cache.put(key, value);
}
} finally {
lock.unlock();
}
}
}
return value;
}
}
// Using StampedLock for read-write operations
public class OptimizedBuffer {
private final StampedLock lock = new StampedLock();
private byte[] data;
public byte[] read() {
long stamp = lock.tryOptimisticRead();
byte[] result = data.clone();
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
result = data.clone();
} finally {
lock.unlockRead(stamp);
}
}
return result;
}
public void write(byte[] newData) {
long stamp = lock.writeLock();
try {
this.data = newData.clone();
} finally {
lock.unlockWrite(stamp);
}
}
}
// Enable JFR (Java Flight Recorder)
java -XX:+UnlockCommercialFeatures \
-XX:+FlightRecorder \
-XX:StartFlightRecording=duration=60s,filename=recording.jfr \
MyApplication
// Enable JMX monitoring
java -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
MyApplication
// Using Micrometer for metrics
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
}
@Service
public class OrderService {
private final Counter orderCounter;
private final Timer processTimer;
public OrderService(MeterRegistry registry) {
this.orderCounter = registry.counter("orders.processed");
this.processTimer = registry.timer("order.processing.time");
}
public void processOrder(Order order) {
orderCounter.increment();
processTimer.record(() -> {
// Process order
});
}
}
@Test
public void performanceTest() {
int iterations = 1000;
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
// Operation to test
}
long endTime = System.nanoTime();
long durationMs = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
double avgTimeMs = (double) durationMs / iterations;
assertTrue("Average time should be less than 1ms", avgTimeMs < 1.0);
}
// JMH benchmark
@State(Scope.Thread)
public class StringConcatenationBenchmark {
@Param({"100", "1000", "10000"})
private int size;
private String[] elements;
@Setup
public void setup() {
elements = new String[size];
for (int i = 0; i < size; i++) {
elements[i] = String.valueOf(i);
}
}
@Benchmark
public String stringConcat() {
String result = "";
for (String element : elements) {
result += element;
}
return result;
}
@Benchmark
public String stringBuilder() {
StringBuilder sb = new StringBuilder(size * 4);
for (String element : elements) {
sb.append(element);
}
return sb.toString();
}
}
Performance optimization in Java applications requires a systematic approach, starting with proper measurement and profiling to identify bottlenecks. While there are many optimization techniques available, focus on those that provide the most significant impact for your specific use case.
Remember that premature optimization can lead to more complex, harder-to-maintain code without meaningful performance benefits. Always measure, optimize where it matters most, and continue monitoring performance in production to ensure your optimizations remain effective as your application evolves.