This episode covers performance tuning: profiling with JFR and async-profiler, tuning the heap and JVM flags with the G1 and ZGC collectors, optimizing startup time and memory footprint, and tuning cache and connection pools.

A slow application rarely needs new technology — it usually needs to find out where the time is lost. Episode 16 covers performance and JVM optimizations: how to measure, find bottlenecks, and then optimize your Spring Boot application with the right knowledge.
You'll learn to profile with JFR and async-profiler, tune JVM flags and choose a garbage collector, speed up startup, shrink the memory footprint, and manage connections correctly.
JFR is the JVM's built-in profiler that records runtime events — memory allocations, GC, threads, and hot methods — with very low overhead. Record a session while the application runs:
jcmd 1234 JFR.start name=profil duration=60s \
filename=profil.jfrjcmd 1234 JFR.start name=profil duration=60s filename=profil.jfr records profiling for 60 seconds into the file profil.jfr. Open that file in JDK Mission Control to analyze which methods consume time and memory.
async-profiler samples CPU and allocations without stopping threads. A single command can produce a flame graph:
./async-profiler -d 60 -o flamegraph -f flamegraph.html <pid>The command ./async-profiler -d 60 -o flamegraph -f flamegraph.html <pid> samples for 60 seconds and produces an interactive flame graph. From there you can immediately see the most expensive call paths.
One of the most common decisions: how much heap to give. The JVM default sets the heap as a percentage of total memory, but for containers it's better to set it explicitly:
java -Xms512m -Xmx1024m \
-XX:MaxMetaspaceSize=256m \
-jar target/belajar-spring-boot.jar-Xms sets the initial heap size, -Xmx the maximum, and -XX:MaxMetaspaceSize caps the metaspace. Don't give the heap more than the container's memory — the JVM can run out of memory because of GC and metaspace.
Java 21 supports two modern garbage collectors:
java -XX:+UseZGC -jar target/belajar-spring-boot.jarBefore switching collectors, measure your application's profile first. ZGC excels at latency, while G1 is more efficient for general throughput. The decision should be based on profiling data, not trends.
Spring Boot starts many beans at startup. Lazy initialization defers bean creation until first use, speeding up startup at the cost of a little extra latency on the first request:
spring:
main:
lazy-initialization: trueThis technique is useful for applications that need fast startup — for example serverless functions or auto-scaling — but think about the consequences: configuration errors only appear when a bean is used, not at startup.
A few steps toward a smaller memory footprint:
-Xmx according to real needs, not as high as possible.A cache reduces database load by storing the results of frequently repeated calls. Enable @EnableCaching and mark the methods that need caching:
@Service
public class ItemService {
@Cacheable("items")
public Item findById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ItemNotFoundException(id));
}
}The items cache stores the result of findById so subsequent calls don't touch the database. Add a provider like Caffeine for a fast in-memory cache, and use @CacheEvict to clear the cache when data changes.
HikariCP is Spring Boot's default connection pool. Set its size to fit your needs — not as large as possible:
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
max-lifetime: 1800000maximum-pool-size sets the maximum number of connections, connection-timeout the wait time for a connection, and max-lifetime the maximum lifetime of a connection. An oversized pool just wastes database resources.
Episode 16 equipped you with performance tuning: profiling with JFR and async-profiler, tuning the heap and JVM flags with the G1 or ZGC collector, optimizing startup and memory footprint, and tuning caches and connection pools.
Key takeaways:
-Xmx must match the container memory — don't make it too large.@Cacheable reduces database load.In the next episode, episode 17, we'll discuss testing and quality assurance — unit testing with JUnit 5 and Mockito, integration testing with @SpringBootTest and @WebMvcTest, contract testing with Spring Cloud Contract, and Testcontainers for databases and dependencies.