JVM Architecture and Performance Tuning
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.jarorjava.basemodule (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:
- Verification: Ensures bytecode is valid and does not violate security constraints
- Preparation: Allocates memory for static fields and initializes them to default values
- 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
- What happens during the verification phase of class loading?
- Why does the JVM use generational garbage collection?
- What is the difference between -Xms and -Xmx?
- How does the JIT compiler decide which methods to compile?
- 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
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