A Java loop cycles through the same four rooms: how the JVM lays out memory and collects it, how threads see each other's writes, how HashMap actually works, and whether you have caught up to virtual threads. The questions repeat because they separate people who memorized the API from people who know the mechanism. This list comes from the rubrics behind our Java modules and real loop reports.
At a glance, what each area tends to open with:
| Area | Typical question | The lesson |
|---|---|---|
| JVM memory & GC | heap vs stack, the OOM taxonomy, G1 vs ZGC | JVM memory & GC |
| Concurrency | synchronized vs volatile, happens-before | Java concurrency |
| Collections | HashMap internals, equals/hashCode | Java collections |
| Loom | virtual threads, carrier pinning | Java concurrency |
JVM memory and garbage collection
The floor question is where things live. Objects and arrays sit on the heap; the reference variable and primitives in a frame sit on the per-thread stack. Deep recursion blows the stack and throws StackOverflowError, which is a different area and a different failure from running out of heap.
The OutOfMemoryError taxonomy
Interviewers read your diagnosis, not the keyword. Each message points somewhere specific:
"Java heap space" -> allocation with no room, GC can't free enough
"GC overhead limit exceeded" -> >98% time in GC recovering <2% heap
"Metaspace" -> too many loaded classes (redeploys, proxies)
"unable to create new native thread" -> OS thread / native memory limit, NOT heap
Trap the last one is native, not heap, so a bigger -Xmx does nothing, and a smaller heap can leave more native room for thread stacks. Reaching for -Xmx on a thread-exhaustion OOM is the answer that caps the score.
A leak in a garbage-collected language
The senior framing: reachable is not the same as in-use. GC never collects an object a root still reaches, so a leak in Java is a reachability bug, not a GC bug. Static collections that grow without bound, listeners never unregistered, and ThreadLocal left in pooled threads are the usual suspects. Raising -Xmx delays the OOM, it never fixes it.
Follow-up "which collector, and why?" G1 (default since Java 9) splits the heap into regions and collects the highest-garbage ones first against a pause goal. ZGC uses colored pointers for sub-millisecond pauses that are largely independent of heap size, at a throughput cost. Say low-pause, not zero-pause: ZGC still stops the world briefly at mark boundaries.
Study JVM memory & GC.
Concurrency and the memory model
synchronized vs volatile
synchronized gives mutual exclusion plus visibility. volatile gives visibility and ordering only, never exclusion. The canonical trap lives in the gap:
volatile int count;
void tick() {
count++; // WRONG: read-modify-write is three steps; volatile makes each
// step visible but does not make the trio atomic. Updates are lost.
}
final AtomicInteger count = new AtomicInteger();
void tick() {
count.incrementAndGet(); // RIGHT: one atomic read-modify-write.
// LongAdder wins under heavy contention.
}
Trap a volatile counter incremented by many threads still loses updates. Naming "three-valued" or "happens-before" earns nothing on its own; showing that ++ is three actions earns the point.
happens-before
The Java Memory Model gives correctness as a partial order. Without a happens-before edge, the compiler, CPU, and cache may reorder and keep values thread-local. The edges interviewers want: a monitor unlock happens-before the next lock on the same monitor, a volatile write happens-before every later read of it, and Thread.start() happens-before the thread's first action. Double-checked locking is broken without a volatile field, because another thread can publish a partially constructed object.
Study Java concurrency. For the language-agnostic layer underneath it (races, mutexes, deadlock and the Coffman conditions), see Concurrency fundamentals.
Virtual threads
Project Loom (GA in JDK 21) makes blocking cheap. A virtual thread mounts on a carrier platform thread; when it blocks on JDK I/O or Thread.sleep, it unmounts and frees the carrier, so millions of blocked tasks cost almost nothing. What Loom does not buy you is parallelism for CPU-bound work, which still needs about one thread per core.
Follow-up "what pins a carrier?" In JDK 21 through 23, blocking inside a synchronized block pins the carrier and silently caps throughput; the fix was to swap in a ReentrantLock. JEP 491 reworked the monitor in JDK 24 so synchronized no longer pins. Getting that version boundary right is the senior tell. Also: do not pool virtual threads, create one per task.
Study Java concurrency.
Collections, and the HashMap question
Nearly every Java loop asks how HashMap works. Hash the key, mask the hash to a bucket index, walk the bucket comparing with equals. Capacity is a power of two, so index = hash & (n - 1) replaces a modulo, and the default load factor 0.75 triggers a resize-and-rehash once size passes capacity times load factor.
The equals/hashCode contract
// Override hashCode without equals, or mutate a key after inserting it:
map.put(user, order);
user.setId(99); // WRONG: changes the field hashCode() reads.
map.get(user); // returns null; the entry sits in the old bucket.
Trap equal objects must return equal hash codes, but the reverse is not required. "Different objects must have different hashCodes" is false; unequal keys are allowed to collide. Override both together, from the same effectively-immutable fields.
The senior follow-up
Follow-up "what happens to a bucket with many collisions?" Since Java 8 a bucket that reaches 8 entries treeifies into a red-black tree for O(log n) lookup, but only if table capacity is at least 64; below that it resizes instead. Saying "8 entries become a tree" without the capacity gate is the common wrong answer. The point of treeification is to cap the worst case against hash-collision denial of service.
While you are here: ArrayList beats LinkedList in practice because contiguous memory hits the cache, and each linked node also costs 24-plus bytes of pointer overhead. Fail-fast iterators are a debugging aid built on a non-volatile modCount, not a concurrency guarantee.
Study Java collections.
The rest of the backend loop
A Java role is a backend role, so the loop rarely stops at the language. The same day usually includes a data round on SQL fundamentals, an interface round on API design covering idempotency keys and pagination, and, at senior, a backend system design session where the JVM knowledge above becomes an argument about tail latency and pool sizing.
Red flags treating a native-thread OOM as a heap problem, claiming
volatilemakes a counter thread-safe, quoting Java 7 segment locking forConcurrentHashMap, and describing treeification without the capacity-64 gate. Any one of these caps the score no matter how fluent the rest sounds. The free notes run from the working mental model to the internals; the diagnostic tells you which band you clear today.