JVM Performance Tuning (Beginner-Friendly Guide)

Goal: Build a small Java app with intentional performance problems, and use real-world tools to analyze and understand:

  • Garbage Collection (GC) behavior
  • Thread issues and deadlocks
  • Profiling and bottleneck detection with Java Flight Recorder (JFR)

Step 1: Set Up a Java App with a Memory Leak

What’s a memory leak in Java?

In Java, memory leaks occur when objects are unintentionally kept in memory because they’re still referenced, preventing the Garbage Collector (GC) from reclaiming them.

Sample Code

import java.util.*;

public class MemoryLeakDemo {
    static List<byte[]> memoryLeak = new ArrayList<>();
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            memoryLeak.add(new byte[1024 * 1024]); // allocate 1MB chunks
            Thread.sleep(100); // wait 100ms
        }
    }
}

Compile and Run

javac MemoryLeakDemo.java
java -Xms128m -Xmx512m MemoryLeakDemo
FlagDescription
-Xms128mStart with 24MB heap
-Xmx512mLimit max heap to 64MB

After a while (less than 1 minute), you can observe the app crashes due to OOM:

App crash due to out of memory issue

Step 2: Observe and Tune GC Behavior

Why enable GC logs?

GC logs show how often garbage collection runs, how much memory is reclaimed (or not), and whether your app pauses due to GC activity.

🔍 Basic GC Logging

java -Xms128m -Xmx512m \
     -XX:+UseG1GC \
     "-Xlog:gc*" \
     MemoryLeakDemo
GC activity after turning on GC Log

Example log line:

GC(474) Pause Full (G1 Compaction Pause) 511M->511M(512M) 2.594ms
FieldMeaning
Pause FullIndicates a Full GC (expensive)
511M->511MNo memory was freed (leak!)
512MMax heap size
2.594msPause duration

Redirect logs for offline analysis

-Xlog:gc:file=gc.log

Step 3: Analyze Thread Dumps

What’s a thread dump?

A snapshot of all JVM threads, their states, and call stacks. Critical for diagnosing deadlocks and app hangs.

Example: Blocked Thread

public class ThreadLockDemo {
    public static void main(String[] args) throws InterruptedException {
        final Object lock = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (true) {}
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("This will never be printed");
            }
        });

        t1.start();
        Thread.sleep(500);
        t2.start();
    }
}

Get a Thread Dump

jps           # find Java process ID
jstack <PID>  # print thread dump

Look for:

  • Thread-0 in RUNNABLE (holding lock)
  • Thread-1 in BLOCKED (waiting for lock)

Here is the actual thread dump of the program:

Thread dump example

Step 4: Profile with Java Flight Recorder (JFR)

What is JFR?

A low-overhead profiler built into the JVM. It tracks memory allocations, CPU usage, GC activity, thread contention, and more.

Start JFR Recording

java -XX:+UnlockCommercialFeatures \
     -XX:+FlightRecorder \
     -XX:StartFlightRecording=duration=60s,filename=app.jfr \
     MemoryLeakDemo

Or use jcmd on a running process:

jcmd <pid> JFR.start duration=60s filename=app.jfr

Analyze in Java Mission Control (JMC)

  1. Download: jdk.java.net/jmc
  2. Open app.jfr
  3. Look at:
    • Memory allocation by class
    • GC pause times
    • CPU usage (hot methods)
    • Thread contention (locks)

Simulate High CPU Usage

public class CpuHog {
    public static void main(String[] args) {
        while (true) {
            Math.pow(Math.random(), Math.random());
        }
    }
}

Profile it with JFR or VisualVM and:

  • Spot hot methods
  • Insert Thread.sleep(1); to reduce CPU load

Leave a Comment