JVM internals for senior engineers — class loader types and the parent-delegation model, how delegation prevents class spoofing, when and how it is deliberately broken, the loading/linking/initialisation pipeline, defineClass and custom class loaders, classes with the same name in one JVM, where class metadata lives, bytecode verification, bytecode inspection tools, invoke* instructions and invokedynamic, object header layout and compressed oops.
Published September 25, 2026
These questions are asked to engineers who debug ClassCastException: X cannot be cast to X, application-server classloader leaks, or container memory sizing. Tie every answer to a production symptom you could diagnose with it.
Short answer: Since Java 9, there are three built-in loaders:
java.base and others) from the runtime image. In Java code it appears as null.Frameworks add their own (web-application loaders in Tomcat, Spring Boot's nested-JAR loader, OSGi bundles, plugin loaders).
Parent delegation: loadClass(name) works in three steps:
findLoadedClass).findClass.protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
try { c = getParent() != null ? getParent().loadClass(name) : findBootstrapClassOrNull(name); }
catch (ClassNotFoundException ignored) { }
if (c == null) c = findClass(name); // our own lookup, last
}
if (resolve) resolveClass(c);
return c;
}
}
Learn it in depth → JVM Memory Areas
Short answer: Because the parents are asked first, a request for java.lang.String always resolves to the bootstrap loader's real class. A malicious java/lang/String.class on the classpath is never used. On top of that:
java.* packages from non-bootstrap loaders (SecurityException: Prohibited package name);The result is that core types (String, Class, Object) and their invariants can't be replaced.
Short answer: Yes. Override loadClass to do child-first (parent-last) loading, or use the thread context class loader. Legitimate uses:
WEB-INF/lib first, so different applications can use different library versions. Java EE and Jakarta API classes are still delegated to the parent.DriverManager, JNDI and ServiceLoader are loaded by parent loaders, but must find application-level implementations. They use the thread context class loader (Thread.currentThread().getContextClassLoader()), which is an inversion of delegation.Key points to cover:
ClassCastException, LinkageError) and classloader leaks on redeploy.Short answer: Per the JVM specification, there are three phases:
java.lang.Class mirror on the heap.<clinit> (static initialisers, and static field assignments in textual order), exactly once, thread-safely, and triggered by the first active use: new, a static method call, a static field access (except compile-time constants), reflection, or initialising a subclass.Key points to cover:
ExceptionInInitializerError once, and NoClassDefFoundError on every later attempt.defineClass() for in a custom class loader?Short answer: defineClass(name, bytes, off, len[, protectionDomain]) is the protected final method that turns a byte array into a Class object owned by this loader. The JVM parses and verifies the bytes, and records this loader as the defining loader. A custom loader typically overrides findClass: it fetches the bytes (from a database, the network, an encrypted JAR, or generated code), then calls defineClass.
public class PluginClassLoader extends ClassLoader {
private final Path pluginDir;
public PluginClassLoader(Path pluginDir, ClassLoader parent) { super("plugin", parent); this.pluginDir = pluginDir; }
@Override protected Class<?> findClass(String name) throws ClassNotFoundException {
Path file = pluginDir.resolve(name.replace('.', '/') + ".class");
try {
byte[] bytes = Files.readAllBytes(file);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
Key points to cover:
findClass, not loadClass, to keep parent delegation intact (unless you deliberately want child-first loading).ClassLoader.registerAsParallelCapable()) for concurrency.MethodHandles.Lookup.defineClass or defineHiddenClass (Java 15), instead of custom loaders.URLClassLoader covers the common "load from JARs" case.Short answer: Yes, if different class loaders define them. At runtime, a class's identity is the pair (fully qualified name, defining class loader). So com.acme.Plugin loaded by loader A and loader B are different types. Casting an instance of one to the other throws the famous ClassCastException: com.acme.Plugin cannot be cast to com.acme.Plugin. The loader constraints can also produce LinkageError: loader constraint violation.
This happens with web applications sharing a container, plugin systems, hot reload (DevTools: the old and new restart loaders), and duplicate JARs in different loaders. The fix is to put shared API types in a common parent loader.
Short answer: Class metadata includes:
Since Java 8, it lives in Metaspace: native memory, off the Java heap, allocated per class loader, and freed when the loader is collected. It replaced PermGen. With compressed class pointers, class structures sit in the Compressed Class Space (1 GB by default).
Key points to cover:
Class mirror object.Tuning: -XX:MaxMetaspaceSize (a leak guard), MetaspaceSize (the first GC threshold). Diagnose with jcmd <pid> VM.metaspace.
Short answer: Before a class is linked, the verifier checks that its bytecode is structurally and type-safe, so malformed or malicious class files can't corrupt the JVM. It checks that:
int as a reference, which would forge pointers);final rules are respected.Since Java 7, class files carry StackMapTable frames, so the type-checking verifier runs in a single fast pass.
Common trap: turning verification off with -Xverify:none or -noverify for "faster startup". It's deprecated since Java 13, and unsafe. Use CDS or AOT caches for startup instead.
Short answer:
javap -c -v -p MyClass: disassembles bytecode, and shows the constant pool, stack map frames and flags.java.lang.classfile), finalised in Java 24, for parsing and generating class files.-XX:+PrintCompilation, JITWatch, and -XX:+PrintAssembly (needs the hsdis plugin).invokestatic, invokevirtual, and so on)?Short answer:
invokestatic: static methods. The target is known at link time, so there's no receiver and no dispatch.invokespecial: constructors (<init>), super.method() calls, and (before Java 11) private methods. Non-virtual: the exact method is fixed.invokevirtual: instance methods on classes. Dynamic dispatch through the receiver class's vtable (a fixed slot index per method).invokeinterface: interface methods. Dispatch through itables, which is slightly costlier, because the slot varies per implementing class.invokedynamic: call sites linked at runtime by a bootstrap method, which returns a CallSite/MethodHandle. Used for lambdas (LambdaMetafactory), string concatenation (Java 9+), records' toString/equals/hashCode, and pattern-matching switch.Key points to cover:
invokevirtual/invokeinterface, but can't be overridden.Short answer:
ClassLoader (or URLClassLoader), passing an explicit parent.findClass(String): locate the bytes, then call defineClass.findResource/findResources if the loader also serves resources.Class.forName(name, true, loader), and only cast to interfaces loaded by a common parent.URLClassLoader.close()) and drop all references, so its classes can be unloaded.Use cases: plugins, per-tenant isolation of script engines, loading encrypted or generated code, hot reloading. Remember that it's a modularity boundary, not a security boundary.
Short answer: In HotSpot on 64-bit JVMs, every object starts with a header:
Then come the fields, reordered by the JVM for alignment (longs and doubles, then ints, then shorts and chars, then bytes and booleans, then references), with the whole object padded to 8 bytes. So new Object() takes 16 bytes, and a Boolean wrapper takes 16 bytes to hold one bit.
Key points to cover:
-XX:+UseCompactObjectHeaders) merge the class pointer into the mark word. That gives 8-byte headers, and typically 10–20% less heap for object-heavy applications.ClassLayout.parseClass(Order.class).toPrintable().Short answer: Compressed ordinary object pointers: on 64-bit JVMs, object references are stored as 32-bit offsets, scaled by the object alignment (8 bytes), and shifted when they're decoded. That lets 32-bit references address up to about 32 GB of heap, while saving memory and cache footprint on every reference field and array slot. It's on by default when -Xmx is below about 32 GB. HotSpot uses zero-based compressed oops when the heap fits below 4 GB or 32 GB in virtual address space, which avoids even the base addition.
Common trap: raising -Xmx from 31 GB to 33 GB disables compressed oops. All references double to 8 bytes, so you can end up with less effective capacity than before. Either stay below about 31 GB, or go well above (48 GB or more). You can also raise -XX:ObjectAlignmentInBytes=16 to extend the compressed range to 64 GB, at some padding cost.
Q: What's the difference between ClassNotFoundException and NoClassDefFoundError?
A: ClassNotFoundException is a checked exception from explicit loading (Class.forName, loadClass) when the class can't be found. NoClassDefFoundError is an error when a class that was present at compile time can't be loaded or linked at runtime: it's missing from the classpath, or its static initialiser failed earlier.
Q: When are classes unloaded? A: Only when their defining class loader becomes unreachable, together with all its classes and instances. Then the GC can reclaim them, and free their Metaspace. Classes loaded by the bootstrap, platform and application loaders are never unloaded.
Q: What is the thread context class loader, and why does it cause leaks?
A: It's a per-thread loader that frameworks use to find application classes from library code. Pooled threads that keep a reference to an old web application's loader (through the TCCL, or ThreadLocals) keep the entire old application in memory after a redeploy, which is a classic Metaspace leak.
Q: What are hidden classes (Java 15)?
A: Classes defined with Lookup.defineHiddenClass. They can't be found by name or linked by other classes, can be unloaded independently, and are meant for framework-generated code (lambda proxies, dynamic proxies, Groovy or JRuby runtimes). They replaced the internal Unsafe.defineAnonymousClass.