Performance and JVM Tuning
Measure first — JVM memory (heap, stack, Metaspace), OutOfMemoryError vs StackOverflowError, garbage collectors and G1, JVM flags, HikariCP, thread pools and @Async, caching, N+1 queries and profiling tools.
#Introduction
Performance tuning separates a good Java developer from a senior one. Most production issues aren't caused by bugs — they're caused by slow SQL, memory leaks, thread starvation, connection-pool exhaustion, poor garbage collection, excessive object creation, blocking I/O, cache misses and inefficient algorithms.
Interviewers frequently ask: Why is your Spring Boot application slow? How do you identify bottlenecks? Heap vs stack? Explain G1GC. What causes OutOfMemoryError and StackOverflowError? What is HikariCP? How do you tune thread pools? What is the N+1 query problem? What tools do you use?
#Performance Layers
Performance problems can exist at any layer:
#Performance Tuning Strategy
Senior engineers follow this order:
#JVM Memory Model
One of the most important interview topics.
- JVM memory
- Heap
- Stack (one per thread)
- Metaspace
- Code cache
- Other native memory
Heap
Stores objects — arrays, collections, Spring beans, Hibernate entities. In User user = new User();, the User object lives on the heap.
Stack
Each thread has its own stack of method frames, holding local variables — primitive values and references to heap objects (not the objects themselves).
public void calculate() {
int x = 10; // x lives on the stack
}Heap vs Stack
Interview favourite.
| Heap | Stack |
|---|---|
| Stores objects | Stores method frames (locals, references) |
| Shared by all threads | Thread-specific |
| Garbage collected | Freed automatically when a method returns |
| Larger | Smaller |
| Slower allocation/access | Faster |
Metaspace
Since Java 8, class metadata (classes, methods, bytecode information) lives in Metaspace, in native memory. Before Java 8 it was the PermGen space inside the heap.
Memory Allocation
#OutOfMemoryError
Common reasons: memory leaks, very large collections, unbounded caches, loading huge files into memory, and a heap that's simply too small.
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024]);
}Eventually: java.lang.OutOfMemoryError: Java heap space.
#StackOverflowError
Usually caused by infinite (or very deep) recursion:
public void test() {
test();
}Each call adds another stack frame until the thread's stack is exhausted.
#Garbage Collection
GC removes unreachable objects from the heap:
Heap Generations
Modern collectors organize memory differently internally, but the traditional generational model is useful for understanding GC:
| Region | Contents | GC |
|---|---|---|
| Young generation | New objects | Frequent (minor GC) |
| Old generation | Long-lived objects | Less frequent (major/mixed GC) |
G1 Garbage Collector
The default collector since JDK 9 (on server-class machines). It splits the heap into many regions, collects in parallel and targets low, predictable pause times. Benefits: predictable pauses, large-heap support and good overall throughput.
Other Garbage Collectors
Interview favourite.
| Collector | Best for |
|---|---|
| Serial GC | Small applications |
| Parallel GC | Maximum throughput |
| G1 GC | General purpose (default) |
| ZGC | Very low pause times |
| Shenandoah | Low latency |
#Common JVM Options
-Xms2G
-Xmx2G
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200| Option | Purpose |
|---|---|
-Xms | Initial heap size |
-Xmx | Maximum heap size |
-XX:+UseG1GC | Enable the G1 collector |
-XX:MaxGCPauseMillis | Pause-time target |
#Spring Boot Startup Performance
Slow startup usually comes from too many beans, heavy initialization, slow database connections, expensive @PostConstruct methods and a large classpath.
Lazy Initialization
spring.main.lazy-initialization=trueBeans are created only when first needed. ✅ Faster startup. ❌ The first request may be slower (and configuration errors surface later).
#HikariCP
Interview favourite. The default Spring Boot connection pool.
Why a Connection Pool?
Much faster.
Common Hikari Settings
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000(The default maximum pool size is 10. Bigger isn't automatically better — the database has limits too.)
#Thread Pools
Never create unlimited threads.
new Thread(...).start(); // ❌
ExecutorService executor = Executors.newFixedThreadPool(10); // ✅Spring example:
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean
ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
return executor;
}
}@Async
@Async
public void sendEmail() {
}Useful for emails, notifications, reports and image processing. (Like @Transactional, @Async works through a proxy — self-invocation won't run it asynchronously.)
#Caching
Instead of hitting the database on every request, check a cache first. Benefits: lower latency and reduced database load.
@Cacheable("users")
public User getUser(Long id) {
}(Enable it with @EnableCaching.)
Cache Providers
| Provider | Best use |
|---|---|
| Caffeine | In-memory (local) cache |
| Redis | Distributed cache |
| Hazelcast | Distributed in-memory data grid |
#Database Performance
Most performance issues originate here. Always:
- Use indexes
- Avoid
SELECT * - Fetch only the required columns
- Use pagination
- Analyze execution plans
N+1 Query Problem
Interview favourite.
Solution: use fetch joins or entity graphs where appropriate:
@Query("""
SELECT o
FROM Order o
JOIN FETCH o.customer
""")This retrieves the related data in a single query.
Pagination
SELECT * FROM USERS; -- ❌ loads the whole table
SELECT * FROM USERS LIMIT 20; -- ✅Spring Data:
Page<User> users = repository.findAll(pageable);#Code-Level Tips
Logging
logger.info("User " + user.getName()); // ❌ string is built even if INFO is disabled
logger.info("User {}", user.getName()); // ✅ SLF4J formats only when needed(The argument expression itself still runs; for expensive arguments, guard with if (logger.isDebugEnabled()).)
Avoid Unnecessary Object Creation
for (int i = 0; i < 100000; i++) {
new Object();
}Unnecessary allocations increase GC pressure.
Immutable Objects
Immutable objects are generally thread-safe, easier to cache and less error-prone. Use immutable DTOs (e.g. records) where appropriate.
#Profiling and Diagnostics
Interview favourite. Common tools:
- Java Flight Recorder (JFR)
- Java Mission Control (JMC)
- VisualVM
- Eclipse Memory Analyzer (MAT)
- JConsole
- async-profiler
Use profilers before making optimizations.
Thread Dump
Diagnoses deadlocks, blocked threads, high CPU and thread starvation: jstack <PID>, or the Actuator threaddump endpoint.
Heap Dump
Diagnoses memory leaks, large collections and duplicate objects: jmap -dump:live,format=b,file=heap.hprof <PID>, or the Actuator heapdump endpoint. Analyze with Eclipse MAT.
GC Logs
Enable with -Xlog:gc* and analyze pause times, allocation rate, heap usage and full-GC frequency.
JVM Monitoring
GC, heap, threads and CPU are usually monitored through Micrometer → Prometheus → Grafana.
#Performance Analysis Workflow
Common Bottlenecks
Slow SQL, missing indexes, too many threads, an exhausted connection pool, GC pressure, large object creation and network delay.
#Common Mistakes
- A huge heap — large heaps can increase GC pause times. Size the heap from measurements, not guesswork.
- Unlimited thread creation — leads to high CPU, memory exhaustion and context-switching overhead. Use managed thread pools.
- No connection pool — opening a database connection per request is expensive. Use HikariCP.
- No cache — cache frequently accessed data when appropriate.
- Loading entire tables — always paginate large result sets.
- Ignoring SQL execution plans — verify indexes with execution plans, not assumptions.
- Optimizing without measuring — always profile first.
#Enterprise Performance Architecture
#Interview Questions
Q1What is the difference between heap and stack?
| Heap | Stack |
|---|---|
| Objects | Method frames |
| Shared | Thread-local |
| Garbage collected | Released automatically |
Q2What causes OutOfMemoryError?
Memory leaks, excessive object creation, very large collections and insufficient heap.
Q3What causes StackOverflowError?
Usually infinite or excessively deep recursion.
Q4Which GC is the default in modern Java?
The G1 garbage collector (since JDK 9).
Q5What is HikariCP?
The default high-performance JDBC connection pool used by Spring Boot.
Q6Why use connection pooling?
To reuse database connections instead of opening a new one for every request.
Q7What is the N+1 query problem?
Fetching a list of parents and then running one extra query per parent entity, resulting in many unnecessary database calls.
Q8Why use @Cacheable?
To reduce repeated database access for frequently requested data.
Q9What tools do you use for performance analysis?
JFR, JMC, VisualVM, Eclipse MAT, async-profiler, Micrometer, Prometheus and Grafana.
Q10What is the first step in performance tuning?
Measure the application and identify the bottleneck before optimizing.
#Key Takeaways
- Performance tuning must be measurement-driven.
- Know the JVM memory model: heap, stack and Metaspace.
- G1 is the default collector and balances throughput with pause times.
- HikariCP improves database performance through connection pooling.
- Thread pools and
@Asyncmanage concurrent work efficiently. - Caching, good SQL and avoiding N+1 queries are among the highest-impact optimizations.
- Profile with JFR, JMC, VisualVM, MAT or async-profiler before changing code.
- Monitor continuously with Micrometer, Prometheus and Grafana.