JVM internals, garbage collection, thread lifecycle, memory model, locks, thread pools, CompletableFuture — everything examiners test at Lead level.
| Area | Stores | Scope | GC Managed? | OOM Risk |
|---|---|---|---|---|
| Heap | Objects, arrays, class instances | All threads share | Yes | Yes — most common |
| Stack | Local vars, method frames, primitives | Per thread | No | StackOverflowError |
| Metaspace | Class metadata, static fields | JVM-wide | Partial | Yes — class leaks |
| Code Cache | JIT compiled native code | JVM-wide | No | Deoptimisation |
| PC Register | Current instruction address | Per thread | No | No |
What is the difference between Heap and Stack in Java? Where are objects stored?
Person p = new Person() — the reference p is on Stack, the Person object is on Heap.Many candidates say "primitives are always on Stack". Wrong — a primitive field inside an object lives on the Heap with the object.
What changed from PermGen (Java 7) to Metaspace (Java 8)? Why?
OutOfMemoryError: PermGen space in apps that dynamically load many classes (OSGi, app servers, Groovy).-XX:MaxMetaspaceSize).1. OS loads JVM (libjvm.so / jvm.dll) 2. Bootstrap ClassLoader loads core java.* classes from rt.jar / modules 3. JVM creates main thread + sets up heap, stack, method area 4. Main class is loaded by Application ClassLoader 5. Bytecode verified → Interpreter begins execution 6. JIT watches hot methods (invocation count threshold ~10,000) 7. Hot methods compiled to native machine code → stored in Code Cache 8. GC runs in background threads managing Heap
| Phase | What Happens | Interview Detail |
|---|---|---|
| Loading | Read .class bytecode → create Class object in Heap | Parent delegation model: always asks parent first |
| Verification | Bytecode verifier checks for safety violations | Prevents malicious/corrupted bytecode execution |
| Preparation | Allocate memory for static fields, set default values | static int x → x=0 here, not yet your assigned value |
| Resolution | Resolve symbolic references to direct references | Can be lazy (JIT time) or eager |
| Initialisation | Run static initializers & assign static field values | <clinit> is thread-safe (synchronised by JVM) |
What is the parent delegation model in ClassLoading? Why does it exist?
Before loading a class, a ClassLoader always delegates to its parent first. Only if parent can't find it does the child load it. This ensures:
Breaking parent delegation (e.g. in OSGi, servlet containers) requires custom ClassLoader — common in app servers like Tomcat.
Single-threaded GC. Single CPU / small heap only.
Multiple GC threads. Best for batch jobs.
Concurrent mark-sweep. No compaction = fragmentation over time.
Region-based. Set pause goal. Best balance for services.
Concurrent everything. Sub-millisecond pauses. Future standard.
G1 collects regions with highest garbage % first (Garbage-First). Mixed collections target both Young and Old regions.
Why is G1GC preferred over CMS for payment microservices at Wells Fargo?
-XX:MaxGCPauseMillis=200 — G1 honours it. CMS has no pause goal.For SLA-bound payment APIs where a 2-second pause = P1 incident, G1GC's bounded pause guarantee is a compliance requirement.
Mention ZGC for next-gen: sub-millisecond pauses, fully concurrent — ideal for real-time trade processing. But higher CPU overhead than G1GC.
# Production G1GC tuning for payment service -XX:+UseG1GC -Xms2g -Xmx4g # set both equal → no resize pauses -XX:MaxGCPauseMillis=150 # target pause goal (ms) -XX:G1HeapRegionSize=16m # for 4GB heap; must be power of 2 -XX:G1NewSizePercent=20 # min Young gen 20% -XX:G1MaxNewSizePercent=40 # max Young gen 40% -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heap.hprof -Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m # Diagnose OOM jmap -dump:format=b,file=heap.hprof <pid> # heap dump jstack <pid> # thread dump jstat -gcutil <pid> 1000 # GC stats every 1s
Thread 1 writes flag=true to its CPU cache. Main memory not updated yet. Thread 2 reads stale false from its own cache.
Why does volatile NOT guarantee thread safety? What does it actually provide?
volatile provides exactly two guarantees:
But NOT atomicity. The operation count++ is three bytecode instructions: GETFIELD, IADD, PUTFIELD. Two threads can interleave between GET and PUT → lost update.
When volatile IS sufficient: a single flag written by one thread, read by many. Example: volatile boolean running = true; — one writer, multiple readers. Safe.
When volatile is NOT sufficient: compound read-modify-write operations. Use AtomicInteger (CAS) or synchronized.
Saying "volatile makes operations atomic" is wrong. Many candidates confuse visibility and atomicity. This answer alone can fail a lead-level candidate.
// ❌ Race condition — volatile doesn't help here private volatile int count = 0; public void increment() { count++; // READ → ADD → WRITE (3 ops, not atomic!) } // ✅ Safe — AtomicInteger uses CAS (Compare-And-Swap) private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // hardware CAS — truly atomic } // ✅ volatile IS safe for flag pattern (one writer, many readers) private volatile boolean running = true; public void stop() { running = false; } // one write public void run() { while(running) { /*work*/ } } // reads only // ✅ Double-Checked Locking — volatile REQUIRED for correctness private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { // first check (no lock) synchronized (Singleton.class) { if (instance == null) // second check (with lock) instance = new Singleton(); } } return instance; }
// ❌ DEADLOCK — Thread A holds L1 wants L2, Thread B holds L2 wants L1 synchronized(lock1) { synchronized(lock2) { /* ... */ } // Thread A } synchronized(lock2) { synchronized(lock1) { /* ... */ } // Thread B — deadlock! } // ✅ FIX 1: Consistent lock ordering synchronized(lock1) { // ALWAYS acquire lock1 before lock2 synchronized(lock2) { /* both threads follow same order */ } } // ✅ FIX 2: tryLock with timeout (ReentrantLock) if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) { try { if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) { try { /* critical section */ } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } // ✅ FIX 3: Use CAS / lock-free for payment counters AtomicLong balance = new AtomicLong(1000L); balance.addAndGet(-amount); // no lock needed
What are the four conditions for a deadlock? How would you detect one in production?
Coffman Conditions (all 4 must hold simultaneously):
Detection in production: jstack <pid> dumps all threads — JVM marks deadlocked threads with "Found one Java-level deadlock". Set up alerts on thread dump analysis in APM (Dynatrace/AppDynamics).
Flow: Task arrives → core threads busy? → queue → queue full? → create extra threads (up to max) → max reached? → RejectionPolicy
| Parameter | Meaning | Production Advice |
|---|---|---|
| corePoolSize | Threads always alive even if idle | CPU-bound: = Runtime.getRuntime().availableProcessors() |
| maximumPoolSize | Max threads under load | IO-bound: can be higher (e.g. 4×CPU). CPU-bound: keep = core |
| keepAliveTime | Idle extra threads die after this | 60s default. Tune for burst patterns |
| workQueue | Queue for pending tasks when all threads busy | ⚠️ NEVER use unbounded LinkedBlockingQueue in prod! |
| RejectionHandler | What to do when queue + max threads full | CallerRunsPolicy = natural backpressure (preferred) |
Executors.newFixedThreadPool(10) uses unbounded LinkedBlockingQueue. Under high load, tasks queue indefinitely → OOM. Always use ThreadPoolExecutor with bounded queue and a rejection policy in production financial systems.
// ✅ Production-safe thread pool for payment processing ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, // corePoolSize 20, // maximumPoolSize 60L, TimeUnit.SECONDS, // keepAliveTime new ArrayBlockingQueue<>(500), // BOUNDED queue — critical! new ThreadFactory() { // named threads for jstack private final AtomicInteger i = new AtomicInteger(); public Thread newThread(Runnable r) { return new Thread(r, "payment-worker-" + i.incrementAndGet()); } }, new ThreadPoolExecutor.CallerRunsPolicy() // backpressure! ); // Difference: submit() vs execute() executor.execute(task); // fire-and-forget, no return value Future<?> f = executor.submit(task); // returns Future — can get result/exception
| Rejection Policy | Behaviour | Use Case |
|---|---|---|
| AbortPolicy (default) | Throws RejectedExecutionException | Fail fast — let caller handle |
| CallerRunsPolicy | Caller thread runs the task itself | Natural backpressure — preferred for payments |
| DiscardPolicy | Silently drops the task | Never for financial data! |
| DiscardOldestPolicy | Drops oldest queued task, retries new | Low-priority event pipelines |
// Pattern 1: Sequential pipeline CompletableFuture.supplyAsync(() -> fetchPayment(id), executor) .thenApplyAsync(PaymentService::validate, executor) .thenApplyAsync(PaymentService::process, executor) .whenComplete((result, ex) -> { if (ex != null) handleError(ex); else sendNotification(result); }); // Pattern 2: Parallel calls + combine (fraud check + account check) CompletableFuture<Boolean> fraud = CompletableFuture.supplyAsync(() -> checkFraud(p)); CompletableFuture<Boolean> balance = CompletableFuture.supplyAsync(() -> checkBalance(p)); CompletableFuture<Void> both = CompletableFuture.allOf(fraud, balance); both.join(); // wait for both // Pattern 3: anyOf — first to respond wins (multiple FX rate providers) CompletableFuture.anyOf(provider1, provider2, provider3) .thenApply(rate -> convertCurrency(amount, (Double) rate)); // Pattern 4: timeout (Java 9+) CompletableFuture.supplyAsync(() -> callExternalBank()) .orTimeout(3, TimeUnit.SECONDS) // throws TimeoutException .completeOnTimeout(fallback, 3, TimeUnit.SECONDS); // returns fallback
What thread pool does CompletableFuture use by default? Why can that be dangerous?
By default, CompletableFuture.supplyAsync() uses ForkJoinPool.commonPool() — a JVM-wide shared pool.
Danger: all CompletableFutures in your app share this pool. If a long-running task (e.g. slow DB call) fills the pool, all async operations in the JVM are starved — including health checks and other microservices on the same JVM.
Best practice: always pass a custom Executor — especially for IO-bound operations.
It uses daemon threads. If your main thread exits, tasks may not complete. In Spring Boot this usually isn't an issue, but in standalone apps — be careful.
Idle workers steal tasks from the tail of busy workers' deques. Minimises CPU idle time for recursive divide-and-conquer algorithms.
Red = locked bucket | Dark = tree node (bucket > 8 entries) | Normal = linked list
Explain ConcurrentHashMap internal changes from Java 7 to Java 8. Why was it redesigned?
Java 7: Divided into 16 Segments, each a mini-hashtable with its own ReentrantLock. Max 16 concurrent writes. Heavy memory overhead.
Java 8: Eliminated Segments entirely. Uses:
Result: near-ConcurrentSkipList performance with HashMap-like simplicity.
| Collection | Thread Safe? | Strategy | Best Use Case | Avoid When |
|---|---|---|---|---|
| HashMap | No | None | Single thread, local method | Any shared state |
| Hashtable | Yes | Synchronized every method | Legacy code only | New code (too slow) |
| ConcurrentHashMap | Yes | Bucket-level CAS + sync | High-concurrency read/write maps | Need iterator consistency |
| CopyOnWriteArrayList | Yes | Copy array on every write | Rare writes, many reads (event listeners) | Frequent writes (expensive!) |
| LinkedBlockingQueue | Yes | Two locks (head+tail) | Producer-consumer queues | Need random access |
| ConcurrentLinkedQueue | Yes | CAS — lock-free | High-throughput queues | Need blocking behaviour |
// ConcurrentHashMap — computeIfAbsent (atomic) ConcurrentHashMap<String, List<Payment>> paymentsByAccount = new ConcurrentHashMap<>(); paymentsByAccount.computeIfAbsent(accountId, k -> new CopyOnWriteArrayList<>()) .add(payment); // atomic check-then-act // BlockingQueue — Producer-Consumer for payment events BlockingQueue<PaymentEvent> queue = new LinkedBlockingQueue<>(1000); // Producer thread queue.put(event); // blocks if queue full — backpressure! queue.offer(event, 100, TimeUnit.MS); // timeout variant // Consumer thread PaymentEvent e = queue.take(); // blocks until available // ThreadLocal — per-thread request context (no synchronization needed) private static final ThreadLocal<RequestContext> ctx = new ThreadLocal<>(); ctx.set(new RequestContext(userId, traceId)); // set in filter ctx.get(); // access anywhere in call stack ctx.remove(); // MUST remove — prevents memory leak!
In thread pools, threads are reused. If you set a ThreadLocal value and don't call remove(), the next request on the same thread sees stale data. Worse — in app servers, this causes a classloader memory leak. Always clean up in a finally block or a servlet filter.
What happens when you run java HelloWorld?
JVM starts → Bootstrap CL loads core classes → App CL loads HelloWorld.class → bytecode verified → main thread created → interpreter runs bytecode → JIT compiles hot methods → GC manages heap throughout.
How does JIT compilation work? What are C1 and C2 compilers?
JVM starts interpreted. JIT profiles hot methods (invocation counter). C1 = client compiler, fast compilation, light optimisations. C2 = server compiler, slower but aggressive optimisations (inlining, loop unrolling, escape analysis). HotSpot uses tiered compilation: interpret → C1 → C2 for hottest code.
What causes OutOfMemoryError? Name 3 types.
Difference between Callable and Runnable?
Runnable.run() returns void, can't throw checked exceptions. Callable.call() returns a value (wrapped in Future), can throw checked exceptions. Use Callable when you need a result or need to propagate checked exceptions.
What is a race condition? Give a real example.
Outcome depends on thread scheduling order. Example: two threads read balance=100, both deduct 50 → both write 50 back → final balance 50 instead of 0. Lost update. Fix with synchronization or AtomicLong.compareAndSet().
What is the happens-before relationship?
A happens-before guarantee ensures that all actions (writes) before a certain point are visible to all actions after. Key rules: program order, monitor lock/unlock, volatile write/read, thread start/join, object construction completion.
What is livelock? How is it different from deadlock?
Deadlock: threads are blocked waiting. Livelock: threads keep responding to each other and changing state but make no progress. Like two people in a corridor stepping side-to-side to let each other pass forever. Both are infinite waits but livelock threads are "active".
What is thread starvation?
A thread never gets CPU time because higher-priority threads monopolise the scheduler, or an unfair lock means some threads always win. Example: with default unfair ReentrantLock, a heavily contested lock can starve some threads. Fix: use fair lock mode or redesign access pattern.
When would you use synchronized vs ReentrantLock?
How does ConcurrentHashMap.compute() differ from get() + put()?
get() + put() is not atomic — another thread can modify between the two calls (check-then-act race condition). compute() / computeIfAbsent() / merge() are atomic — the compute function executes under bucket lock. Use compute() for atomic read-modify-write on a CHM entry.
What is a Stop-The-World (STW) pause? Why does it happen?
JVM halts all application threads so GC can safely traverse the object graph without concurrent modifications. Necessary for consistent heap scanning. G1GC minimises STW to concurrent marking phases, keeping major STW only for evacuation pauses. ZGC makes almost everything concurrent — STW under 1ms.
What is String Pool? Where does it live in Java 8+?
String literals are interned in the String pool to avoid duplicate objects. In Java 7 and earlier, String pool was in PermGen. From Java 7u45+, it moved to the Heap — so interned strings are GC-eligible. String.intern() forces pooling of runtime strings.