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
| Region | Contents | Thread Safety |
|---|---|---|
| Heap | Objects, arrays | Shared |
| Metaspace | Class metadata | Shared |
| Thread Stack | Local variables, call frames | Per-thread |
| PC Register | Current instruction address | Per-thread |
| Native Stack | Native method calls | Per-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();
}