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 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) |
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
// 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.
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 |
// 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.
// 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
// 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
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
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 |
# 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







