Skip to content

JVM Architecture and Performance Tuning

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about JVM Architecture and Performance Tuning. We cover key concepts, practical examples, and best practices to help you master this topic.

The Engine Behind Java

The Java Virtual Machine is the engine that makes Java platform-independent. When you compile a .java file, the compiler produces bytecode (.class files) that runs on any JVM implementation regardless of the underlying hardware and operating system. Understanding JVM internals helps you write better code, diagnose performance problems, and tune applications for maximum throughput.

The JVM has three main subsystems: the class loader subsystem, the runtime data areas, and the execution engine. Each plays a critical role in loading, verifying, executing, and optimizing Java bytecode.

flowchart TB
    subgraph JVM[Java Virtual Machine]
        subgraph CL[Class Loader Subsystem]
            CL1[Loading] --> CL2[Linking] --> CL3[Initialization]
        end
        subgraph RDA[Runtime Data Areas]
            MethodArea[Method Area]
            Heap[Heap]
            Stack[Stack]
            PC[PC Register]
            NMS[Native Method Stack]
        end
        subgraph EE[Execution Engine]
            Interpreter --> JIT[JIT Compiler]
            JIT --> Cache[Code Cache]
            GC[Garbage Collector]
        end
        CL3 --> RDA
        RDA --> EE
    end

Class Loader Subsystem

The class loader subsystem has three phases: loading, linking, and initialization.

Loading

The class loader reads the binary representation of a class from its fully qualified name. Java has three built-in class loaders:

  • Bootstrap Class Loader: Loads core JDK classes from rt.jar or java.base module (written in native code)
  • Platform Class Loader: Loads classes from the JDK module path
  • Application Class Loader: Loads classes from the application classpath
public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println("String loader: " +
            String.class.getClassLoader()); // null (bootstrap)
        System.out.println("Demo loader: " +
            ClassLoaderDemo.class.getClassLoader()); // AppClassLoader
        System.out.println("Platform loader: " +
            ClassLoader.getPlatformClassLoader());
    }
}

Output:

String loader: null
Demo loader: jdk.internal.loader.ClassLoaders$AppClassLoader@...
Platform loader: jdk.internal.loader.ClassLoaders$PlatformClassLoader@...

Linking

Linking performs three steps:

  1. Verification: Ensures bytecode is valid and does not violate security constraints
  2. Preparation: Allocates memory for static fields and initializes them to default values
  3. Resolution: Resolves symbolic references to direct references (optional, can occur during execution)

Initialization

Executes static initializers and static field assignments in the order they appear in the source code.

Runtime Data Areas

The JVM divides memory into several areas at runtime.

Heap

The heap is the runtime data area where all objects and arrays live. It is shared across all threads and managed by the garbage collector. The heap is divided into generations:

  • Young Generation: Newly created objects (further split into Eden, S0, S1)
  • Old Generation (Tenured): Long-lived objects promoted from young generation
  • Metaspace (Java 8+): Class metadata (replaced PermGen in Java 7)
public class MemoryDemo {
    public static void main(String[] args) {
        Runtime rt = Runtime.getRuntime();
        System.out.println("Max memory: " + rt.maxMemory() / 1024 / 1024 + " MB");
        System.out.println("Total memory: " + rt.totalMemory() / 1024 / 1024 + " MB");
        System.out.println("Free memory: " + rt.freeMemory() / 1024 / 1024 + " MB");
        
        byte[] bigArray = new byte[10 * 1024 * 1024]; // 10 MB
        System.out.println("After allocation - Free: " +
            rt.freeMemory() / 1024 / 1024 + " MB");
    }
}

Output:

Max memory: 4096 MB
Total memory: 256 MB
Free memory: 248 MB
After allocation - Free: 238 MB

Stack

Each thread has its own JVM stack containing stack frames. Each method call creates a new stack frame holding local variables, operand stack, and frame data. Stack frames are destroyed when the method completes.

PC Register

Each thread has a Program Counter register that contains the address of the currently executing JVM instruction.

Native Method Stack

Stores native method information for calls made through the Java Native Interface (JNI).

Garbage Collection

The JVM's garbage collector automatically reclaims memory occupied by unreachable objects. Java offers multiple GC implementations:

Collector Focus Best For
Serial Single-threaded, small heaps Desktop apps, small data sets
Parallel (Throughput) Multi-threaded, high throughput Batch processing, large heaps
G1 (Garbage First) Low pause times, balanced Web servers, default since Java 9
ZGC (Low Latency) Sub-millisecond pause times Large heaps, latency-sensitive
Shenandoah Concurrent compaction Applications needing consistent pause times

GC Tuning Example

# Using G1 with explicit tuning
java -Xms4g -Xmx4g \
    -XX:+UseG1GC \
    -XX:MaxGCPauseMillis=50 \
    -XX:G1HeapRegionSize=16m \
    -XX:+PrintGCDetails \
    -jar myapp.jar

JIT Compilation

The Just-In-Time compiler improves performance by compiling frequently executed bytecode into native machine code. The JVM uses profiling to identify hot methods and applies increasingly aggressive optimizations.

public class JITDemo {
    static long sum() {
        long total = 0;
        for (int i = 0; i < 1_000_000; i++) {
            total += i;
        }
        return total;
    }
    
    public static void main(String[] args) {
        // Warm up the JIT
        for (int i = 0; i < 10_000; i++) {
            sum();
        }
        
        long start = System.nanoTime();
        sum();
        long end = System.nanoTime();
        System.out.println("JIT-compiled time: " + (end - start) + " ns");
    }
}

Output:

JIT-compiled time: 12345 ns

After 10,000 iterations, the JIT compiles sum() to native code, making subsequent calls orders of magnitude faster.

Performance Tuning Flags

Heap Sizing

# Heap sizing
-Xms512m              # Initial heap size
-Xmx4g                # Maximum heap size
-XX:NewRatio=3        # Old:Young ratio (3 means 3:1)
-XX:SurvivorRatio=6   # Eden:Survivor ratio (6 means 6:2)
-XX:MetaspaceSize=256m # Metaspace initial size

GC Logging

# GC logging (Java 9+ unified logging)
-Xlog:gc*:file=gc.log:time,uptime
-Xlog:gc+heap=debug
-Xlog:gc+age=trace

Diagnostic Flags

# Diagnostic flags
-XX:+PrintCommandLineFlags  # Show effective flags
-XX:+PrintFlagsFinal        # Show all JVM flags
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof

Common Mistakes

1. Setting -Xms Too Low

A small initial heap causes the JVM to repeatedly resize the heap during startup, increasing garbage collection frequency and degrading performance.

2. Ignoring GC Logs

GC logs provide invaluable insights into heap usage, pause times, and allocation patterns. Always enable GC logging in production.

3. Using Too Many GC Threads

Parallel GC uses -XX:ParallelGCThreads (default = CPU cores). On shared servers, too many GC threads can starve application threads.

4. Assuming Default GC is Optimal

The default G1 collector is good for most workloads, but ZGC may serve latency-sensitive applications better, while Parallel GC may suit batch processing.

# Check which GC is active
java -XX:+PrintCommandLineFlags -version

5. Not Taming Metaspace

Without a MaxMetaspaceSize limit, class loading (especially in frameworks like Spring) can cause metaspace to grow until a native OS limit is hit.

6. Overlooking JIT Code Cache

JIT-compiled code is stored in the code cache. Running out of code cache causes the JIT to disable compilation, degrading performance.

-XX:ReservedCodeCacheSize=256m
-XX:+PrintCodeCache

Practice Questions

  1. What happens during the verification phase of class loading?
  2. Why does the JVM use generational garbage collection?
  3. What is the difference between -Xms and -Xmx?
  4. How does the JIT compiler decide which methods to compile?
  5. What is the purpose of survivor spaces in the young generation?

Challenge: Write a program that monitors JVM memory and GC activity using java.lang.management.MemoryMXBean and GarbageCollectorMXBean. Display heap usage, GC count, and GC time every 5 seconds.

FAQ

What is the difference between the JVM, JRE, and JDK?

The JVM runs Java bytecode. The JRE includes the JVM and core libraries. The JDK includes the JRE plus development tools like javac, jar, and jlink.

How does escape analysis improve performance?

Escape analysis determines whether an object is used only within a method. If so, the JVM allocates it on the stack instead of the heap, reducing GC pressure.

What is a safepoint?

A safepoint is a point in execution where all threads have stopped at a known state. The JVM uses safepoints for GC, bias lock revocation, and other global operations.

Why does the JVM have both an interpreter and a JIT compiler?

The interpreter starts executing code immediately without startup delay. The JIT compiler identifies hot methods and compiles them to native code for long-running performance.

What is biased locking?

Biased locking optimizes for single-threaded access to an object by biasing the lock toward the first thread that acquires it, reducing CAS operations. It is disabled by default since Java 15.

Mini Project: JVM Diagnostics Tool

Create a command-line tool that connects to a running JVM Process using com.sun.management.HotSpotDiagnosticMXBean (or ManagementFactory) and prints:

  • Heap and non-heap memory usage
  • Current GC algorithm and statistics
  • Number of loaded classes and threads
  • JVM start time and uptime
  • Top 10 system properties

Use jps and jcmd or the Attach API to discover and connect to running JVM processes.

What's Next

Understanding the JVM prepares you to appreciate the evolution of the language itself. In the next lesson, we will survey Java Version Features from Java 8 through Java 21, highlighting the key language and API changes in each release.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro