How strings sit in memory (compact strings), JIT and other JVM optimisations, class loaders and parent delegation, class unloading, class loading and Metaspace, memory leaks and how to find them, the Java Memory Model and visibility, GC and circular references, and where static data lives.
Published September 25, 2026
At this level, interviewers expect you to connect JVM internals to production symptoms: Metaspace growth after redeploys, a heap that climbs until OutOfMemoryError, a flag one thread never sees. Answer with the mechanism, and then the tool you'd use to confirm it.
Short answer: A String object holds a byte[] plus a coder flag. Since Java 9 (compact strings), text that fits in Latin-1 uses one byte per character, and anything else uses UTF-16 (two bytes per character). Strings are immutable, cache their hash code, and literals are interned in the string pool, which lives on the regular heap (since Java 7).
Key points to cover:
String wrapped a char[], with 2 bytes per character even for ASCII. The change roughly halved string memory in typical applications.-XX:+UseStringDeduplication).substring has copied its characters since Java 7u6. The old behaviour shared the parent's array, and could leak huge strings.Learn it in depth → JVM Memory Areas
Short answer: The JVM starts out interpreting bytecode, profiles what actually runs, and JIT-compiles the hot methods to optimised native code. The main optimisations:
Tiered compilation (the C1 compiler, then C2) balances startup speed against peak performance.
Key points to cover:
Learn it in depth → GC Tuning & Diagnostics
Short answer: The same mechanisms as Q2. The follow-up is what makes them fail:
Key points to cover:
-XX:+PrintCompilation shows JIT activity.Short answer: A class loader finds a class's bytes and turns them into a Class object. Loading is lazy: a class loads when it's first actively used. There are three built-in loaders, arranged in a parent-delegation hierarchy:
java.base classes. It's native, and appears as null in Java code.A loader first asks its parent, and loads the class itself only if the parent can't. The phases are: loading → linking (verification, preparation, resolution) → initialisation (static initialisers run).
Key points to cover:
com.x.Foo to com.x.Foo" errors in app servers and plugin systems.LaunchedClassLoader for nested JARs, and DevTools' restart class loader.Short answer: Not directly. A class is unloaded only when its defining class loader becomes unreachable, and so do all its classes, their instances and their Class objects. Classes loaded by the bootstrap, platform or application loaders are effectively never unloaded.
Key points to cover:
ThreadLocal, a JDBC driver registered in DriverManager, a static cache or a running thread, keeps the whole old class loader alive. That's the classic "Metaspace grows after each redeploy" leak.Short answer: Yes, but only through class-loader unloading, as in Q5. To verify it:
-Xlog:class+unload.jcmd <pid> VM.metaspace.Short answer: Each loaded class consumes Metaspace (native memory) for its metadata, bytecode, constant pool and JIT data. Its Class object and static fields live on the heap. Large frameworks, dynamically generated proxies (CGLIB, lambdas, Hibernate) and class-loader leaks all increase this footprint.
Key points to cover:
-XX:MaxMetaspaceSize, so that a leak fails fast instead of consuming container memory.-Xmx.Short answer: In production, the symptoms are OutOfMemoryError: Metaspace, or a pod killed for exceeding its memory limit while the heap looks fine. The causes: too many generated classes (for example, unbounded dynamic proxies or scripting engines), or class-loader leaks on redeploy. Mitigations:
-XX:NativeMemoryTracking=summary plus jcmd VM.native_memory) to see where off-heap memory goes.Short answer: The garbage collector frees unreachable objects automatically. But a Java "memory leak" is objects that are still reachable but no longer needed, and no GC can fix that. Typical causes:
ThreadLocal values left in thread pools.HashMap keys.Key points to cover:
remove() for ThreadLocals in finally.Learn it in depth → Memory Leaks in Java
Short answer:
jcmd <pid> GC.heap_dump file.hprof, or automatically with -XX:+HeapDumpOnOutOfMemoryError.Other tools: JFR (the Old Object Sample event finds long-lived objects cheaply in production), VisualVM, JProfiler and YourKit, async-profiler's allocation mode, and jmap -histo for a quick class histogram.
Short answer: The JMM defines what values a read is allowed to see when several threads share variables. It allows compilers and CPUs to reorder and cache operations for speed, except across happens-before edges:
volatile write → later reads of that variable;Thread.start() → the actions of the started thread;join() on it;final-field freeze at the end of a constructor.If two accesses to shared data aren't ordered by happens-before, and at least one is a write, you have a data race, and the results are unpredictable.
Key points to cover:
Learn it in depth → Volatile and the Java Memory Model
Short answer: Without synchronisation, a write made by one thread may never become visible to another thread, or may become visible late or out of order. The JIT can hoist a read out of a loop, keep a value in a register, or reorder writes. The classic example is a stop flag the worker thread never sees:
class Worker implements Runnable {
private boolean running = true; // not volatile
public void run() { while (running) { } } // the JIT may turn this into while(true)
void stop() { running = false; } // may never be observed
}
Key points to cover:
volatile, synchronized or locks, atomic classes, or the concurrent collections. All of them establish happens-before edges.Short answer: Without any trouble. The JVM's collectors use reachability tracing from GC roots, not reference counting. Objects that reference each other in a cycle, but can't be reached from any root, are simply never marked, and they get collected.
Key points to cover:
static keyword affect memory management?Short answer: Static fields are allocated once per class (per class loader), when the class is initialised. Since Java 8 they're stored on the heap, together with the class's java.lang.Class object. The class's metadata lives in Metaspace. Static fields are GC roots for as long as the class is loaded, which in practice means for the application's lifetime. Anything a static field references stays alive.
Common trap: "static data lives in the method area / PermGen". PermGen was removed in Java 8. Statics moved to the heap, and metadata moved to Metaspace.
Key points to cover:
Q: What's the difference between Metaspace and the heap?
A: The heap holds objects, and is limited by -Xmx. Metaspace holds class metadata in native memory. It grows dynamically, and can be capped with -XX:MaxMetaspaceSize.
Q: What is ClassNotFoundException vs NoClassDefFoundError, from a class-loading perspective?
A: ClassNotFoundException means an explicit dynamic load (Class.forName, loadClass) couldn't find the class. NoClassDefFoundError means the JVM needed a class that was present at compile time, but it's missing now, or its static initialisation previously failed.
Q: How do you get a heap dump from a container that just crashed with OOM?
A: Configure -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps, with /dumps mounted on a persistent volume. Otherwise, the dump dies with the container.
Q: What does escape analysis do? A: The JIT determines whether an object ever escapes its method or thread. If not, it can scalar-replace it (no heap allocation at all), and remove locks on it (lock elision). That's one reason short-lived objects are so cheap in Java.