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
- JProfiler - Comprehensive profiling
- VisualVM - Free, good for basic analysis
- Async-profiler - Low overhead CPU/memory profiling
- 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!