Skip to content
Home / Skills / Java / JVM Internals
JA

JVM Internals

Java advanced v1.0.0

JVM Internals

Overview

This skill covers the internal workings of the Java Virtual Machine including class loading, bytecode execution, Just-In-Time (JIT) compilation, memory management, and the garbage collection subsystem. Understanding JVM internals is essential for performance optimization and troubleshooting complex issues.


Key Concepts

JVM Architecture

┌─────────────────────────────────────────────────────────────────┐
│                      JVM Architecture                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    Class Loading                          │  │
│  │  ┌──────────┐   ┌──────────┐   ┌──────────────────────┐  │  │
│  │  │Bootstrap │ → │Extension │ → │  Application/System  │  │  │
│  │  │  Loader  │   │  Loader  │   │       Loader         │  │  │
│  │  └──────────┘   └──────────┘   └──────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    Runtime Data Areas                     │  │
│  │  ┌─────────────────┐  ┌─────────────────────────────┐   │  │
│  │  │   Method Area   │  │         Heap                 │   │  │
│  │  │   (Metaspace)   │  │  ┌────────┐  ┌───────────┐  │   │  │
│  │  │                 │  │  │ Young  │  │    Old    │  │   │  │
│  │  │ - Class info    │  │  │  Gen   │  │    Gen    │  │   │  │
│  │  │ - Method data   │  │  └────────┘  └───────────┘  │   │  │
│  │  └─────────────────┘  └─────────────────────────────────┘  │
│  │                                                           │  │
│  │  Per-Thread:                                              │  │
│  │  ┌──────────┐  ┌─────────────┐  ┌────────────────────┐   │  │
│  │  │  Stack   │  │  PC Register │  │  Native Method    │   │  │
│  │  │ (Frames) │  │              │  │      Stack        │   │  │
│  │  └──────────┘  └─────────────┘  └────────────────────┘   │  │
│  └──────────────────────────────────────────────────────────┘  │
│                              │                                   │
│                              ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                   Execution Engine                        │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌───────────────┐   │  │
│  │  │ Interpreter  │  │  JIT Compiler │  │  GC Subsystem │   │  │
│  │  └──────────────┘  └──────────────┘  └───────────────┘   │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Memory Regions

RegionContentsThread Safety
HeapObjects, arraysShared
MetaspaceClass metadataShared
Thread StackLocal variables, call framesPer-thread
PC RegisterCurrent instruction addressPer-thread
Native StackNative method callsPer-thread

Best Practices

1. Understand Class Loading

Know the class loading hierarchy and when custom loaders are needed.

2. Monitor Metaspace

Track metaspace usage to detect class loading leaks.

3. Analyze JIT Compilation

Use -XX:+PrintCompilation to understand compilation behavior.

4. Profile Before Tuning

Use JFR and async-profiler before making JVM changes.

5. Right-Size Memory Pools

Configure heap, metaspace, and thread stacks appropriately.


Code Examples

Example 1: Class Loading Internals

public class ClassLoadingInternals {
    
    public static void exploreClassLoaders() {
        // Application class
        ClassLoader appLoader = ClassLoadingInternals.class.getClassLoader();
        System.out.println("App class loader: " + appLoader);
        // Output: jdk.internal.loader.ClassLoaders$AppClassLoader
        
        // Platform class loader (replaces Extension in Java 9+)
        ClassLoader platformLoader = appLoader.getParent();
        System.out.println("Platform loader: " + platformLoader);
        // Output: jdk.internal.loader.ClassLoaders$PlatformClassLoader
        
        // Bootstrap loader (native code, returns null)
        ClassLoader bootstrapLoader = platformLoader.getParent();
        System.out.println("Bootstrap loader: " + bootstrapLoader);
        // Output: null
        
        // Core classes loaded by bootstrap
        System.out.println("String loader: " + String.class.getClassLoader());
        // Output: null (bootstrap)
    }
    
    // Custom class loader for plugin isolation
    public static class PluginClassLoader extends URLClassLoader {
        
        public PluginClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent);
        }
        
        @Override
        protected Class<?> loadClass(String name, boolean resolve) 
                throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                // Check if already loaded
                Class<?> c = findLoadedClass(name);
                
                if (c == null) {
                    // Child-first: try to load from plugin first
                    if (name.startsWith("com.plugin.")) {
                        try {
                            c = findClass(name);
                        } catch (ClassNotFoundException e) {
                            // Fall back to parent
                        }
                    }
                    
                    // Parent delegation for other classes
                    if (c == null) {
                        c = super.loadClass(name, false);
                    }
                }
                
                if (resolve) {
                    resolveClass(c);
                }
                
                return c;
            }
        }
    }
    
    // Detecting class loading issues
    public static void monitorClassLoading() {
        // Enable class loading verbose output
        // -verbose:class or -XX:+TraceClassLoading
        
        // Programmatic monitoring
        ClassLoadingMXBean classLoadingBean = ManagementFactory.getClassLoadingMXBean();
        
        System.out.println("Loaded classes: " + classLoadingBean.getLoadedClassCount());
        System.out.println("Total loaded: " + classLoadingBean.getTotalLoadedClassCount());
        System.out.println("Unloaded: " + classLoadingBean.getUnloadedClassCount());
    }
}

Example 2: Memory Management

public class MemoryManagement {
    
    public static void analyzeMemory() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        
        // Heap memory
        MemoryUsage heap = memoryBean.getHeapMemoryUsage();
        System.out.printf("Heap: used=%dMB, committed=%dMB, max=%dMB%n",
            heap.getUsed() / (1024 * 1024),
            heap.getCommitted() / (1024 * 1024),
            heap.getMax() / (1024 * 1024));
        
        // Non-heap (metaspace, code cache, etc.)
        MemoryUsage nonHeap = memoryBean.getNonHeapMemoryUsage();
        System.out.printf("Non-Heap: used=%dMB, committed=%dMB%n",
            nonHeap.getUsed() / (1024 * 1024),
            nonHeap.getCommitted() / (1024 * 1024));
        
        // Memory pool details
        for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
            MemoryUsage usage = pool.getUsage();
            System.out.printf("Pool '%s': type=%s, used=%dKB%n",
                pool.getName(),
                pool.getType(),
                usage.getUsed() / 1024);
        }
    }
    
    // Direct (off-heap) memory
    public static void directMemoryUsage() {
        // Allocate direct buffer (off-heap)
        ByteBuffer direct = ByteBuffer.allocateDirect(1024 * 1024); // 1MB
        
        // Monitor direct memory
        BufferPoolMXBean directPool = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)
            .stream()
            .filter(p -> p.getName().equals("direct"))
            .findFirst()
            .orElseThrow();
        
        System.out.printf("Direct buffers: count=%d, used=%dKB, capacity=%dKB%n",
            directPool.getCount(),
            directPool.getMemoryUsed() / 1024,
            directPool.getTotalCapacity() / 1024);
    }
    
    // Object size estimation
    public static long estimateObjectSize(Object obj) {
        if (obj == null) return 0;
        
        // Using Instrumentation (requires -javaagent)
        // return instrumentation.getObjectSize(obj);
        
        // Rough estimation using reflection
        Class<?> clazz = obj.getClass();
        long size = 16; // Object header (12 bytes + 4 padding typically)
        
        while (clazz != null) {
            for (Field field : clazz.getDeclaredFields()) {
                if (Modifier.isStatic(field.getModifiers())) continue;
                
                Class<?> fieldType = field.getType();
                if (fieldType.isPrimitive()) {
                    size += getPrimitiveSize(fieldType);
                } else {
                    size += 4; // Reference size (compressed oops)
                }
            }
            clazz = clazz.getSuperclass();
        }
        
        // Align to 8 bytes
        return (size + 7) & ~7;
    }
    
    private static int getPrimitiveSize(Class<?> type) {
        if (type == boolean.class || type == byte.class) return 1;
        if (type == char.class || type == short.class) return 2;
        if (type == int.class || type == float.class) return 4;
        if (type == long.class || type == double.class) return 8;
        return 0;
    }
}

Example 3: JIT Compilation Analysis

public class JITCompilation {
    
    /*
     * JIT Compilation Tiers (Tiered Compilation - default):
     * 
     * Level 0: Interpreter
     * Level 1: Simple C1 with full profiling
     * Level 2: Limited C1 with basic profiling
     * Level 3: Full C1 with all profiling
     * Level 4: C2 with aggressive optimizations
     * 
     * JVM flags for analysis:
     * -XX:+PrintCompilation        - Print compilation events
     * -XX:+UnlockDiagnosticVMOptions
     * -XX:+PrintInlining           - Print inlining decisions
     * -XX:+PrintAssembly           - Print generated assembly (needs hsdis)
     * -XX:CompileThreshold=10000   - Invocations before compilation
     */
    
    // Method likely to be inlined
    private int add(int a, int b) {
        return a + b;
    }
    
    // Hot method - will be compiled to native code
    public long hotMethod() {
        long sum = 0;
        for (int i = 0; i < 100_000; i++) {
            sum += add(i, i + 1);
        }
        return sum;
    }
    
    // Checking compilation status
    public static void checkCompilation() {
        CompilationMXBean compilationBean = ManagementFactory.getCompilationMXBean();
        
        if (compilationBean.isCompilationTimeMonitoringSupported()) {
            System.out.println("JIT Compiler: " + compilationBean.getName());
            System.out.println("Total compilation time: " + 
                compilationBean.getTotalCompilationTime() + "ms");
        }
    }
    
    // OSR (On-Stack Replacement) demonstration
    public static void osrDemo() {
        // Long-running loop - OSR will compile while running
        long sum = 0;
        for (int i = 0; i < 100_000_000; i++) {
            sum += i;
            // After threshold, JIT compiles and replaces running loop
        }
        System.out.println(sum);
    }
    
    // Intrinsics - methods replaced with optimized native code
    public static void intrinsicsDemo() {
        // These are replaced by CPU instructions:
        int bits = Integer.bitCount(42);        // POPCNT instruction
        int leading = Integer.numberOfLeadingZeros(42);  // LZCNT
        
        // Math intrinsics
        double sqrt = Math.sqrt(2.0);           // SQRTSD
        double sin = Math.sin(0.5);             // May use x87 or SSE
        
        // String intrinsics
        String s = "hello";
        int hash = s.hashCode();                // Vectorized loop
        boolean equals = s.equals("hello");     // Vectorized comparison
    }
}

Example 4: Garbage Collection Deep Dive

public class GCDeepDive {
    
    public static void monitorGC() {
        // GC beans
        for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
            System.out.printf("GC: %s, collections: %d, time: %dms%n",
                gc.getName(),
                gc.getCollectionCount(),
                gc.getCollectionTime());
        }
        
        // GC notifications
        for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
            NotificationEmitter emitter = (NotificationEmitter) gc;
            emitter.addNotificationListener((notification, handback) -> {
                if (notification.getType().equals(
                        GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
                    
                    GarbageCollectionNotificationInfo info = 
                        GarbageCollectionNotificationInfo.from(
                            (CompositeData) notification.getUserData());
                    
                    GcInfo gcInfo = info.getGcInfo();
                    System.out.printf("GC: %s, action: %s, duration: %dms%n",
                        info.getGcName(),
                        info.getGcAction(),
                        gcInfo.getDuration());
                    
                    // Memory before/after
                    Map<String, MemoryUsage> before = gcInfo.getMemoryUsageBeforeGc();
                    Map<String, MemoryUsage> after = gcInfo.getMemoryUsageAfterGc();
                    
                    for (String pool : after.keySet()) {
                        long freed = before.get(pool).getUsed() - after.get(pool).getUsed();
                        if (freed > 0) {
                            System.out.printf("  %s: freed %dKB%n", pool, freed / 1024);
                        }
                    }
                }
            }, null, null);
        }
    }
    
    // Reference types for GC cooperation
    public static void referenceTypes() {
        Object strongRef = new byte[1024 * 1024]; // Strong - not collected
        
        // Soft reference - collected under memory pressure
        SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
        
        // Weak reference - collected at next GC
        WeakReference<byte[]> weakRef = new WeakReference<>(new byte[1024 * 1024]);
        
        // Phantom reference - for cleanup actions
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        PhantomReference<byte[]> phantomRef = 
            new PhantomReference<>(new byte[1024 * 1024], queue);
        
        // Check reference status
        System.out.println("Soft: " + (softRef.get() != null));
        System.out.println("Weak: " + (weakRef.get() != null));
        
        // Force GC (for demonstration only - don't use in production)
        System.gc();
        
        System.out.println("After GC:");
        System.out.println("Soft: " + (softRef.get() != null)); // Likely still there
        System.out.println("Weak: " + (weakRef.get() != null)); // Likely null
    }
    
    // WeakHashMap for automatic cleanup
    public static void weakHashMapDemo() {
        WeakHashMap<Object, String> cache = new WeakHashMap<>();
        
        Object key1 = new Object();
        Object key2 = new Object();
        
        cache.put(key1, "value1");
        cache.put(key2, "value2");
        
        System.out.println("Before: " + cache.size()); // 2
        
        key1 = null;
        System.gc();
        
        // Allow time for GC
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        
        System.out.println("After: " + cache.size()); // Likely 1
    }
}

Example 5: Thread Stack Analysis

public class ThreadStackAnalysis {
    
    public static void analyzeThreads() {
        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        
        // All thread IDs
        long[] threadIds = threadBean.getAllThreadIds();
        System.out.println("Total threads: " + threadIds.length);
        
        // Thread info with stack traces
        ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds, 10);
        
        for (ThreadInfo info : threadInfos) {
            if (info == null) continue;
            
            System.out.printf("Thread: %s (id=%d, state=%s)%n",
                info.getThreadName(),
                info.getThreadId(),
                info.getThreadState());
            
            // CPU time (if supported)
            if (threadBean.isThreadCpuTimeSupported()) {
                long cpuTime = threadBean.getThreadCpuTime(info.getThreadId());
                System.out.printf("  CPU time: %dms%n", cpuTime / 1_000_000);
            }
            
            // Lock info
            LockInfo lock = info.getLockInfo();
            if (lock != null) {
                System.out.printf("  Waiting on: %s (held by thread %d)%n",
                    lock.getClassName(),
                    info.getLockOwnerId());
            }
            
            // Stack trace
            for (StackTraceElement element : info.getStackTrace()) {
                System.out.println("    at " + element);
            }
        }
    }
    
    // Deadlock detection
    public static void detectDeadlocks() {
        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        
        long[] deadlockedThreads = threadBean.findDeadlockedThreads();
        
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            System.out.println("DEADLOCK DETECTED!");
            
            ThreadInfo[] infos = threadBean.getThreadInfo(deadlockedThreads, true, true);
            for (ThreadInfo info : infos) {
                System.out.printf("Thread %s is deadlocked%n", info.getThreadName());
                System.out.printf("  Waiting for: %s%n", info.getLockInfo());
                System.out.printf("  Held by: thread %d (%s)%n",
                    info.getLockOwnerId(),
                    info.getLockOwnerName());
                
                for (StackTraceElement element : info.getStackTrace()) {
                    System.out.println("    at " + element);
                }
            }
        } else {
            System.out.println("No deadlocks detected");
        }
    }
    
    // Thread contention monitoring
    public static void monitorContention() {
        ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
        
        if (threadBean.isThreadContentionMonitoringSupported()) {
            threadBean.setThreadContentionMonitoringEnabled(true);
            
            // After some time...
            for (ThreadInfo info : threadBean.getThreadInfo(threadBean.getAllThreadIds())) {
                if (info == null) continue;
                
                long blockedTime = info.getBlockedTime();
                long waitedTime = info.getWaitedTime();
                
                if (blockedTime > 100 || waitedTime > 100) {
                    System.out.printf("Thread %s: blocked=%dms, waited=%dms%n",
                        info.getThreadName(),
                        blockedTime,
                        waitedTime);
                }
            }
        }
    }
}

Anti-Patterns

❌ Relying on finalize()

// WRONG - deprecated and unreliable
@Override
protected void finalize() throws Throwable {
    closeResource(); // May never be called!
}

// RIGHT - use try-with-resources or Cleaner
public class Resource implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    
    @Override
    public void close() {
        // Cleanup
    }
}

❌ Calling System.gc()

// WRONG - interferes with GC ergonomics
System.gc();

// RIGHT - let JVM manage GC
// Use appropriate heap sizing instead

Testing Strategies

@Test
void shouldNotLeakClasses() {
    WeakReference<Class<?>> classRef;
    
    try (PluginClassLoader loader = new PluginClassLoader(urls)) {
        Class<?> pluginClass = loader.loadClass("com.plugin.MyPlugin");
        classRef = new WeakReference<>(pluginClass);
    }
    
    // Trigger class unloading
    System.gc();
    Thread.sleep(100);
    
    assertThat(classRef.get()).isNull();
}

References