Module 7
Tuning and Optimizing Java Applications
This work is licensed under CC BY-NC-SA 4.0
© Way-Up 2025
Garbage Collection (GC) automatically reclaims memory from unused objects.
| Collector | Pause Time | Throughput | Use Case |
|---|---|---|---|
| Serial GC | High | Low | Single-threaded, small apps |
| Parallel GC | High | High | Batch processing |
| G1 GC | Medium | Medium | Default (Java 9+), balanced |
| ZGC | Very Low | Medium | Large heaps, low latency |
| Shenandoah | Very Low | Medium | Low latency requirements |
# Serial GC (single CPU)
java -XX:+UseSerialGC -jar app.jar
# Parallel GC (throughput)
java -XX:+UseParallelGC -jar app.jar
# G1 GC (default in Java 9+, balanced)
java -XX:+UseG1GC -jar app.jar
# ZGC (ultra-low latency, Java 15+)
java -XX:+UseZGC -jar app.jar
# Shenandoah (low latency)
java -XX:+UseShenandoahGC -jar app.jar
Recommendation: Start with G1 GC, switch to ZGC if you need < 10ms pauses
# Heap size
java -Xms2g -Xmx4g -jar app.jar
# -Xms: Initial heap size (2GB)
# -Xmx: Maximum heap size (4GB)
# Young generation size
java -Xmn1g -jar app.jar
# -Xmn: Young generation size
# Metaspace (class metadata)
java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar app.jar
# Thread stack size
java -Xss512k -jar app.jar
# Direct memory
java -XX:MaxDirectMemorySize=1g -jar app.jar
# Complete example
java -Xms4g -Xmx4g -Xmn1g -XX:+UseG1GC \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
-jar app.jar
# Enable GC logging (Java 9+)
java -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=100m \
-jar app.jar
# Java 8 GC logging
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
-Xloggc:gc.log -jar app.jar
# Dump heap on OutOfMemoryError
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-jar app.jar
# Enable JMX for remote monitoring
java -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9090 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-jar app.jar
JVisualVM is a free, powerful profiling tool included with JDK.
# Launch JVisualVM
jvisualvm
# Or from command line with PID
jvisualvm --openpid <process-id>
JConsole monitors JVM using JMX.
# Launch JConsole
jconsole
# Connect to remote JVM
jconsole hostname:9090
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.metrics.export.prometheus.enabled=true
Access metrics at: http://localhost:8080/actuator/metrics
Prometheus endpoint: http://localhost:8080/actuator/prometheus
Async-profiler is a low-overhead sampling profiler.
# Download async-profiler
wget https://github.com/async-profiler/async-profiler/releases/download/v2.9/async-profiler-2.9-linux-x64.tar.gz
tar -xzf async-profiler-2.9-linux-x64.tar.gz
# Profile running JVM
./profiler.sh -d 30 -f flamegraph.html <pid>
# Profile specific event
./profiler.sh -e cpu -d 30 -f cpu-flamegraph.html <pid>
./profiler.sh -e alloc -d 30 -f alloc-flamegraph.html <pid>
Generates interactive flame graphs showing where time is spent
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Creates 1000 String objects!
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
// Text blocks
String json = """
{
"name": "John",
"age": 30
}
""";
| Operation | ArrayList | LinkedList | HashSet |
|---|---|---|---|
| get(index) | O(1) | O(n) | - |
| add(element) | O(1)* | O(1) | O(1) |
| add(index, element) | O(n) | O(n) | - |
| remove(index) | O(n) | O(n) | - |
| contains(element) | O(n) | O(n) | O(1) |
Rule: Use ArrayList for random access, LinkedList for frequent insertions/deletions at beginning
List numbers = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
numbers.add(i); // Autoboxing: int → Integer
}
int sum = 0;
for (Integer num : numbers) {
sum += num; // Unboxing: Integer → int
}
int[] numbers = new int[1000000];
for (int i = 0; i < 1000000; i++) {
numbers[i] = i;
}
int sum = 0;
for (int num : numbers) {
sum += num; // No boxing/unboxing
}
public class ExpensiveResource {
private static ExpensiveObject instance;
public static ExpensiveObject getInstance() {
if (instance == null) {
instance = new ExpensiveObject(); // Create only when needed
}
return instance;
}
}
// Or with Supplier (functional approach)
public class Service {
private final Supplier lazySupplier =
Suppliers.memoize(() -> new ExpensiveObject());
public void use() {
ExpensiveObject obj = lazySupplier.get(); // Created on first call
}
}
// Create new connection for each request
Connection conn = DriverManager.getConnection(url, user, password);
// Use connection
conn.close(); // Expensive!
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// Expensive database query
return userRepository.findById(id).orElse(null);
}
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}
@CacheEvict(value = "users", allEntries = true)
public void clearCache() {
// Clear all cache entries
}
}
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users", "products");
}
}
The JIT (Just-In-Time) compiler optimizes hot code paths.
final for constants and methods when possible# Print JIT compilation
java -XX:+PrintCompilation -jar app.jar
JMH (Java Microbenchmark Harness) for accurate benchmarking:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConcatBenchmark {
@Benchmark
public String concatenateWithPlus() {
String result = "";
for (int i = 0; i < 100; i++) {
result += i;
}
return result;
}
@Benchmark
public String concatenateWithStringBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i);
}
return sb.toString();
}
}
#!/bin/bash
# Production-ready JVM settings
JAVA_OPTS=" \
-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1ReservePercent=10 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-XX:+ExitOnOutOfMemoryError \
-Xlog:gc*:file=/logs/gc.log:time,uptime:filecount=10,filesize=100m \
-XX:+UseStringDeduplication \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9090 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false"
java $JAVA_OPTS -jar app.jar
Task: Find and fix a memory leak
Task: Optimize GC for different workloads
Task: Optimize a slow application
In this module, you learned:
Congratulations! You've completed the Advanced Java course!