Performance optimization is both an art and a science. In this post, I share techniques that helped me achieve 10x performance improvements in production systems.

Understanding the JVM

Before optimizing, understand how JVM manages memory:

Heap Memory
├── Young Generation
│   ├── Eden Space
│   └── Survivor Spaces (S0, S1)
└── Old Generation
    └── Tenured Space

Non-Heap Memory
├── Metaspace
├── Code Cache
└── Thread Stacks

JVM Tuning Parameters

For High-Throughput Applications

java -Xms4g -Xmx4g 
     -XX:+UseG1GC 
     -XX:MaxGCPauseMillis=200 
     -XX:+UseStringDeduplication 
     -XX:+ParallelRefProcEnabled 
     -jar application.jar

For Low-Latency Applications (Java 21+)

java -Xms2g -Xmx2g 
     -XX:+UseZGC 
     -XX:+ZGenerational 
     -jar application.jar

Code-Level Optimizations

1. Use StringBuilder for String Concatenation

// Bad - creates multiple String objects
String result = "";
for (String item : items) {
    result += item + ", ";
}

// Good - efficient string building
StringBuilder sb = new StringBuilder();
for (String item : items) {
    sb.append(item).append(", ");
}
String result = sb.toString();

2. Leverage Stream API Wisely

// For small collections - traditional loop may be faster
for (User user : users) {
    if (user.isActive()) {
        activeUsers.add(user);
    }
}

// For large collections - parallel streams
List<User> activeUsers = users.parallelStream()
    .filter(User::isActive)
    .collect(Collectors.toList());

3. Use Appropriate Data Structures

// For frequent lookups - use HashMap
Map<String, User> userMap = new HashMap<>();

// For sorted data - use TreeMap
Map<String, User> sortedUsers = new TreeMap<>();

// For thread-safe operations - use ConcurrentHashMap
Map<String, User> concurrentMap = new ConcurrentHashMap<>();

// For unique elements - use HashSet
Set<String> uniqueIds = new HashSet<>();

4. Database Query Optimization

// Bad - N+1 query problem
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
    Customer customer = order.getCustomer(); // Lazy load - N queries
}

// Good - fetch join
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();

5. Caching Strategy

@Service
public class ProductService {

    @Cacheable(value = "products", key = "#id")
    public Product getProduct(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }

    @CacheEvict(value = "products", key = "#product.id")
    public Product updateProduct(Product product) {
        return productRepository.save(product);
    }
}

Profiling Tools

  1. JProfiler - Comprehensive profiling
  2. VisualVM - Free, good for basic analysis
  3. Async-profiler - Low overhead CPU/memory profiling
  4. JFR (Java Flight Recorder) - Built-in, production-safe

Real-World Results

After applying these optimizations to an e-commerce platform:

  • Response time: 850ms → 120ms

  • Throughput: 500 req/s → 3000 req/s

  • Memory usage: 8GB → 4GB

  • GC pause time: 200ms → 15ms

Performance optimization is a continuous process. Always measure before and after changes!