Advanced Java

JVM Performance

Module 7

Tuning and Optimizing Java Applications

JVM Architecture Overview

JVM Memory Structure

Heap Memory:


Non-Heap Memory:

Garbage Collection Basics

Garbage Collection (GC) automatically reclaims memory from unused objects.


GC Process:

  1. Mark: Identify live objects
  2. Sweep: Remove dead objects
  3. Compact: Defragment memory (optional)

Types of GC:

Garbage Collectors

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

Selecting a Garbage Collector

# 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

JVM Memory Parameters

# 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

GC Logging and Monitoring

# 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

Monitoring Tools: JVisualVM

JVisualVM is a free, powerful profiling tool included with JDK.


Features:


# Launch JVisualVM
jvisualvm

# Or from command line with PID
jvisualvm --openpid <process-id>

Monitoring Tools: JConsole

JConsole monitors JVM using JMX.

# Launch JConsole
jconsole

# Connect to remote JVM
jconsole hostname:9090

Monitor:

Modern Monitoring: Spring Boot Actuator + Micrometer

<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

Profiling with Async-profiler

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

Common Performance Issues

1. Memory Leaks

2. CPU Intensive Operations

3. I/O Bottlenecks

Optimization: String Handling

Bad:

String result = "";
for (int i = 0; i < 1000; i++) {
    result += i; // Creates 1000 String objects!
}

Good:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

Also Good (Java 15+):

// Text blocks
String json = """
    {
        "name": "John",
        "age": 30
    }
    """;

Optimization: Collection Choice

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

Optimization: Avoid Autoboxing

Bad (autoboxing overhead):

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
}

Better (use primitives):

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
}

Optimization: Lazy Initialization

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
    }
}

Optimization: Connection Pooling

Without Pooling (slow):

// Create new connection for each request
Connection conn = DriverManager.getConnection(url, user, password);
// Use connection
conn.close(); // Expensive!

With HikariCP (fast):

<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

Optimization: Caching

@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");
    }
}

JIT Compiler Optimization

The JIT (Just-In-Time) compiler optimizes hot code paths.


JIT does:


Tips:


# Print JIT compilation
java -XX:+PrintCompilation -jar app.jar

Performance Testing with JMH

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();
    }
}

Production JVM Settings

#!/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

Exercise 1: Memory Leak Detection

Task: Find and fix a memory leak


Requirements:

  1. Create an application with a memory leak
  2. Enable heap dump on OOM
  3. Run until OOM occurs
  4. Analyze heap dump with JVisualVM or Eclipse MAT
  5. Identify the leak source
  6. Fix the leak and verify

Exercise 2: GC Tuning

Task: Optimize GC for different workloads


Requirements:

  1. Create a load testing scenario
  2. Test with different GC algorithms
  3. Enable GC logging
  4. Analyze GC logs with GCViewer
  5. Compare pause times and throughput
  6. Choose optimal GC for your workload

Exercise 3: Performance Optimization

Task: Optimize a slow application


Requirements:

  1. Profile with JVisualVM or async-profiler
  2. Identify hot spots (CPU intensive methods)
  3. Optimize algorithms and data structures
  4. Add caching where appropriate
  5. Write JMH benchmarks to verify improvements
  6. Document performance gains

Best Practices Summary

Summary

In this module, you learned:


Congratulations! You've completed the Advanced Java course!

Resources

Slide Overview