Skip to main content

12 - JVM and Performance Tuning

JVM architecture, memory areas, Garbage Collection, JIT compiler, profiling, diagnostics and performance optimization.

What do you think about this article?

At a Glance

JVM architecture, memory areas, Garbage Collection, JIT compiler, profiling, diagnostics and performance optimization.

JVM and Performance Tuning

The Java Virtual Machine (JVM) is the heart of every Java application. Understanding its internals and optimization techniques allows you to write more performant applications and diagnose memory issues.

What You'll Learn

  • JVM Architecture
  • Memory Areas: Stack, Heap, Metaspace
  • Garbage Collection and algorithms
  • JVM tuning with flags
  • Profiling and diagnostics
  • Memory leaks and optimization

JVM Architecture

The JVM consists of three main components: Class Loader, Runtime Data Areas and Execution Engine.

Main JVM Components

Component Function
Class Loader Loads, links and initializes classes
Runtime Data Areas Stack, Heap, Method Area, PC Register
Execution Engine Interpreter, JIT Compiler, Garbage Collector
Class Loading Process
// Class Loading happens in 3 phases:
// 1. Loading - Reads the bytecode (.class)
// 2. Linking - Verifies, Prepares, Resolves
// 3. Initialization - Executes static blocks and initializations

public class ClassLoadingDemo {
    // Static block executed during initialization
    static {
        System.out.println("Class loaded and initialized!");
    }

    public static void main(String[] args) {
        // Get the current ClassLoader
        ClassLoader loader = ClassLoadingDemo.class.getClassLoader();
        System.out.println("ClassLoader: " + loader);

        // ClassLoader hierarchy:
        // 1. Bootstrap ClassLoader (core Java classes)
        // 2. Platform/Extension ClassLoader
        // 3. Application/System ClassLoader (classpath)

        // Dynamic class loading
        try {
            Class<?> clazz = Class.forName("java.util.ArrayList");
            System.out.println("Class loaded: " + clazz.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

Memory Areas

The JVM manages several memory areas, each with a specific purpose.

Runtime Data Areas

Area Content Shared
Heap Objects and arrays Yes (among all threads)
Stack Method frames, local variables No (one per thread)
Metaspace Class metadata, method area Yes
PC Register Current instruction address No (one per thread)
Native Method Stack Stack for native methods No (one per thread)
Stack vs Heap
public class MemoryAreasDemo {
    // Instance variable -> HEAP (inside the object)
    private int instanceVar = 10;

    // Static variable -> METASPACE
    private static String staticVar = "static";

    public void method() {
        // Local primitive variables -> STACK
        int localPrimitive = 5;

        // Local reference -> STACK, object -> HEAP
        String localString = new String("hello");

        // Array: reference in STACK, content in HEAP
        int[] array = new int[10];
    }

    public static void main(String[] args) {
        // Each method call creates a new Stack Frame
        // Stack Frame contains:
        // - Local variables
        // - Operand Stack
        // - Reference to constant pool

        MemoryAreasDemo demo = new MemoryAreasDemo();
        // demo (reference) -> STACK
        // new MemoryAreasDemo() (object) -> HEAP

        demo.method();
    }
}

Heap Structure

Heap Generations
// The Heap is divided into generations:

// YOUNG GENERATION (Eden + Survivor Spaces)
// - Newly created objects go here
// - Frequent and fast Minor GC
// - Divided into: Eden, Survivor 0 (S0), Survivor 1 (S1)

// OLD GENERATION (Tenured)
// - Objects that survived multiple GC cycles
// - Less frequent but slower Major GC
// - Large objects may go directly here

// Flags to size the heap:
// -Xms512m     Initial heap (512 MB)
// -Xmx2g       Maximum heap (2 GB)
// -Xmn256m     Young Generation (256 MB)

// Configuration example:
// java -Xms1g -Xmx4g -Xmn512m MyApplication

// View memory at runtime:
public class HeapDemo {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();

        long maxMemory = runtime.maxMemory();      // -Xmx
        long totalMemory = runtime.totalMemory();  // Current heap
        long freeMemory = runtime.freeMemory();    // Free in current
        long usedMemory = totalMemory - freeMemory;

        System.out.println("Max Memory: " + maxMemory / (1024 * 1024) + " MB");
        System.out.println("Total Memory: " + totalMemory / (1024 * 1024) + " MB");
        System.out.println("Free Memory: " + freeMemory / (1024 * 1024) + " MB");
        System.out.println("Used Memory: " + usedMemory / (1024 * 1024) + " MB");
    }
}

Garbage Collection

The Garbage Collector (GC) automatically frees memory occupied by objects that are no longer reachable.

Objects Eligible for GC
public class GCDemo {
    public static void main(String[] args) {
        // 1. Nullifying reference
        Object obj1 = new Object();
        obj1 = null;  // Now eligible for GC

        // 2. Reassigning reference
        Object obj2 = new Object();  // First object
        obj2 = new Object();         // First object now eligible

        // 3. Objects inside methods
        createObjects();  // Objects created in method eligible after return

        // 4. Island of isolation
        class Node {
            Node next;
        }
        Node a = new Node();
        Node b = new Node();
        a.next = b;
        b.next = a;
        a = null;
        b = null;
        // Both eligible (no external reference)

        // Suggest GC (not guaranteed!)
        System.gc();
        Runtime.getRuntime().gc();
    }

    static void createObjects() {
        Object local = new Object();
        // local eligible for GC after method returns
    }
}

Garbage Collection Algorithms

Available Garbage Collectors

GC Flag Characteristics
Serial GC -XX:+UseSerialGC Single thread, for small applications
Parallel GC -XX:+UseParallelGC Multi-thread, optimizes throughput
G1 GC -XX:+UseG1GC Default Java 9+, low pauses, large heaps
ZGC -XX:+UseZGC Ultra-low pauses (<10ms), huge heaps
Shenandoah -XX:+UseShenandoahGC Low pauses, concurrent compaction
GC Configuration
// Common flags for GC tuning:

// Choose the GC
// -XX:+UseG1GC          (default Java 9+)
// -XX:+UseZGC           (ultra-low pauses)
// -XX:+UseParallelGC    (throughput)

// G1 GC tuning
// -XX:MaxGCPauseMillis=200    Max pause target (default 200ms)
// -XX:G1HeapRegionSize=4m     Region size (1-32 MB)
// -XX:InitiatingHeapOccupancyPercent=45  Concurrent GC threshold

// GC Logging (Java 9+)
// -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=10m

// Complete example:
// java -Xms2g -Xmx4g \
//      -XX:+UseG1GC \
//      -XX:MaxGCPauseMillis=100 \
//      -Xlog:gc*:file=gc.log \
//      MyApplication

// GC statistics at runtime:
import java.lang.management.*;
import java.util.List;

public class GCStatsDemo {
    public static void main(String[] args) {
        List<GarbageCollectorMXBean> gcBeans =
            ManagementFactory.getGarbageCollectorMXBeans();

        for (GarbageCollectorMXBean gc : gcBeans) {
            System.out.println("GC Name: " + gc.getName());
            System.out.println("Collection Count: " + gc.getCollectionCount());
            System.out.println("Collection Time: " + gc.getCollectionTime() + " ms");
            System.out.println();
        }
    }
}

JIT Compilation

The Just-In-Time (JIT) Compiler compiles bytecode to native code at runtime, optimizing the most executed parts of the code.

JIT Compilation Tiers
// The JVM uses Tiered Compilation (default):
// Tier 0: Pure interpreter
// Tier 1: C1 compiler (fast compilation, few optimizations)
// Tier 2: C1 with limited profiling
// Tier 3: C1 with full profiling
// Tier 4: C2 compiler (maximum optimization)

// JIT flags:
// -XX:+TieredCompilation          (default on)
// -XX:TieredStopAtLevel=1         Stop at C1 (fast startup)
// -XX:-TieredCompilation          Only C2 (server apps)
// -XX:CompileThreshold=10000      Compilation threshold

// See what gets compiled:
// -XX:+PrintCompilation

// Example output:
//   123   1       3  java.lang.String::hashCode (55 bytes)
//   125   2       4  java.util.HashMap::hash (20 bytes)
// Columns: timestamp, id, tier, method, bytecode size

// Verify optimizations:
public class JitDemo {
    public static void main(String[] args) {
        // Hot loop - will be compiled by JIT
        long sum = 0;
        for (int i = 0; i < 100_000; i++) {
            sum += calculate(i);
        }
        System.out.println("Sum: " + sum);
    }

    // This method will be inlined by JIT
    static int calculate(int n) {
        return n * 2 + 1;
    }
}

Profiling and Diagnostics

Profiling Tools
// 1. jconsole / jvisualvm - GUI monitoring
// Start with: jconsole or jvisualvm

// 2. jps - List Java processes
// $ jps -l
// 12345 com.example.MyApp

// 3. jstat - GC statistics
// $ jstat -gc <pid> 1000    (every 1000ms)
// $ jstat -gcutil <pid> 1000

// 4. jmap - Memory dump
// $ jmap -histo <pid>        Object list
// $ jmap -dump:file=heap.hprof <pid>   Heap dump

// 5. jstack - Thread dump
// $ jstack <pid>

// 6. Java Flight Recorder (JFR)
// $ java -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp

// Programmatic memory analysis:
import java.lang.management.*;

public class MemoryProfilingDemo {
    public static void main(String[] args) {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();

        // Heap memory
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        System.out.println("Heap:");
        System.out.println("  Init: " + heapUsage.getInit() / (1024*1024) + " MB");
        System.out.println("  Used: " + heapUsage.getUsed() / (1024*1024) + " MB");
        System.out.println("  Committed: " + heapUsage.getCommitted() / (1024*1024) + " MB");
        System.out.println("  Max: " + heapUsage.getMax() / (1024*1024) + " MB");

        // Non-heap (Metaspace)
        MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
        System.out.println("Non-Heap:");
        System.out.println("  Used: " + nonHeapUsage.getUsed() / (1024*1024) + " MB");

        // Thread info
        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        System.out.println("Thread count: " + threadBean.getThreadCount());
        System.out.println("Peak thread count: " + threadBean.getPeakThreadCount());
    }
}

Memory Leaks and Optimization

Common Causes of Memory Leaks
import java.util.*;

public class MemoryLeakExamples {

    // 1. LEAK: Static collections that grow
    private static List<byte[]> cache = new ArrayList<>();

    public void addToCache(byte[] data) {
        cache.add(data);  // Never removed!
    }

    // FIX: Use cache with limit
    // private static LRUCache cache = new LRUCache(100);

    // 2. LEAK: Listeners not removed
    class Button {
        private List<ActionListener> listeners = new ArrayList<>();

        void addListener(ActionListener l) {
            listeners.add(l);
        }

        // Missing removeListener()!
    }

    interface ActionListener { void onClick(); }

    // 3. LEAK: Inner class holds reference to outer
    class Outer {
        byte[] bigData = new byte[10_000_000];

        class Inner {
            // Implicitly holds reference to Outer!
        }

        Inner createInner() {
            return new Inner();
        }
    }

    // FIX: Use static inner class
    // static class Inner { ... }

    // 4. LEAK: Connections/resources not closed
    public void readFile() {
        try {
            // FileInputStream fis = new FileInputStream("file.txt");
            // Read...
            // Missing fis.close()!
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // FIX: try-with-resources
    public void readFileSafe() {
        // try (FileInputStream fis = new FileInputStream("file.txt")) {
        //     // Read...
        // } // Automatically closed
    }

    // 5. LEAK: HashMap with mutable keys
    public void hashMapLeak() {
        Map<List<String>, String> map = new HashMap<>();
        List<String> key = new ArrayList<>();
        key.add("a");

        map.put(key, "value");

        key.add("b");  // Modifies the hashCode!
        // Now map.get(key) won't find the entry
        // The entry remains "orphaned" in the map
    }
}

Optimization Techniques

Performance Best Practices
import java.util.*;

public class PerformanceOptimizations {

    // 1. StringBuilder for concatenations in loops
    public String buildStringBad(int n) {
        String result = "";
        for (int i = 0; i < n; i++) {
            result += i;  // Creates new String every time!
        }
        return result;
    }

    public String buildStringGood(int n) {
        StringBuilder sb = new StringBuilder(n * 4);  // Pre-allocate
        for (int i = 0; i < n; i++) {
            sb.append(i);
        }
        return sb.toString();
    }

    // 2. Initialize collections with capacity
    public void collectionCapacity() {
        // BAD: multiple resizes
        List<String> list = new ArrayList<>();

        // GOOD: pre-allocated
        List<String> listGood = new ArrayList<>(1000);

        // Same for HashMap (consider load factor 0.75)
        Map<String, Integer> map = new HashMap<>((int)(1000 / 0.75) + 1);
    }

    // 3. Avoid boxing/unboxing in loops
    public long sumBad(List<Integer> numbers) {
        long sum = 0;
        for (Integer n : numbers) {
            sum += n;  // Unboxing at each iteration
        }
        return sum;
    }

    public long sumGood(int[] numbers) {
        long sum = 0;
        for (int n : numbers) {
            sum += n;  // No boxing
        }
        return sum;
    }

    // 4. Lazy initialization
    private volatile ExpensiveObject expensive;

    public ExpensiveObject getExpensive() {
        if (expensive == null) {
            synchronized (this) {
                if (expensive == null) {
                    expensive = new ExpensiveObject();
                }
            }
        }
        return expensive;
    }

    class ExpensiveObject {}

    // 5. Object pooling for heavy objects
    // Example: connection pool for database
}

Common JVM Flags

Configuration Flags

Flag Description
-Xms, -Xmx Initial and maximum heap
-Xss Thread stack size (e.g., -Xss512k)
-XX:MetaspaceSize Initial Metaspace
-XX:+HeapDumpOnOutOfMemoryError Automatic dump on OOM
-XX:HeapDumpPath Path for heap dump
-XX:+PrintFlagsFinal Show all flags
Production Configuration
# Example configuration for production web application:

java \
  -Xms4g \
  -Xmx4g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/app/heapdump.hprof \
  -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m \
  -XX:+ExitOnOutOfMemoryError \
  -jar myapp.jar

# Flags for containers (Docker/Kubernetes):
java \
  -XX:+UseContainerSupport \
  -XX:MaxRAMPercentage=75.0 \
  -XX:InitialRAMPercentage=50.0 \
  -jar myapp.jar

Best Practices

Rules for Optimal Performance

  • Measure before optimizing: use profilers
  • Size heap correctly: not too small, not too large
  • Choose the right GC: G1 for general applications
  • Monitor GC: enable logging
  • Avoid memory leaks: close resources, remove listeners
  • Prefer primitives: avoid unnecessary boxing
  • Pre-allocate collections: specify initial capacity
  • Use try-with-resources: to manage resources

Frequently asked questions

What are the three main components of a Java Virtual Machine (JVM)?

The three main components of a JVM are Class Loader, Runtime Data Areas, and Execution Engine.

How does the Garbage Collector (GC) handle memory occupied by objects that are no longer reachable?

The GC automatically frees memory occupied by objects that are no longer reachable.

What is the purpose of JIT Compilation in a JVM?

The Just-In-Time (JIT) Compiler compiles bytecode to native code at runtime, optimizing the most executed parts of the code.

Where can I find information on common JVM flags and best practices for optimization?

Information on common JVM flags and best practices is not explicitly mentioned in this article, but it may be found through other resources.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects