Senior-level edge cases in exception handling (return in try and finally, finally swallowing exceptions, uncaught exceptions, try without catch, constructor failures, suppressed exceptions, precise rethrow, wrapping), memory and GC (reachability vs scope, compile-time string constants, leaks vs retention, System.gc, finalize and Cleaner, escape analysis, static references, heap vs non-heap, Metaspace OOM) and keywords (blank finals, volatile vs synchronized, static/instance initialiser order, this in static context, strictfp, package-private).
Published September 25, 2026
These questions reward precise knowledge of the language specification and the JVM. Several have answers that changed recently:
finalize is deprecated for removal.strictfp is redundant since Java 17.Current answers show you keep up with the platform.
try and finally contain return statements?Short answer: The finally block's return wins.
try block's return value is computed and saved.finally runs, and its return replaces that value.If the try block threw an exception, a return in finally discards the exception entirely.
static int f() {
try { return 1; }
finally { return 2; } // f() returns 2
}
static int g() {
try { throw new IllegalStateException("lost!"); }
finally { return 0; } // g() returns 0; the exception silently disappears
}
Learn it in depth → Exception Handling
finally block prevent an exception from propagating?Short answer: Yes, and that's a bug magnet. If finally:
Keep finally blocks for cleanup only. Better still, use try-with-resources, which preserves the original exception, and attaches close failures as suppressed exceptions.
finally?Short answer: It propagates up the call stack, unwinding each frame, until a handler is found. If none is found, the thread dies:
UncaughtExceptionHandler is invoked (or the thread group's, or the default one). The default prints the stack trace to System.err.java launcher returns a non-zero exit code (1).submit() is captured in the Future. Nothing is printed unless someone calls get(), which is a common source of silently failing background jobs. execute() lets it reach the handler.Key points to cover:
Thread.setDefaultUncaughtExceptionHandler to log errors, and always inspect futures, or use CompletableFuture.exceptionally/whenComplete.try without catch or finally?Short answer: No. A plain try must be followed by at least one catch or a finally. It's a compile error otherwise. The exception is try-with-resources: try (var in = Files.newInputStream(p)) { ... } is legal on its own, because the resource closing acts as an implicit finally.
Short answer: The object is never returned. The new expression completes abruptly, so the reference is never assigned, and the partially built instance becomes unreachable, and eligible for GC. But watch out for these consequences:
catch before rethrowing. Prefer static factory methods that acquire resources after validation.this escaped (registered as a listener, or passed to another thread) before the throw, others can see a half-initialised object.finalize() can resurrect a partially constructed object. Mitigate by making security-sensitive classes final, validating before calling super(...) (possible since Java 25's flexible constructor bodies), or checking in a static factory.ExceptionInInitializerError the first time, then NoClassDefFoundError on every later use of the class.try { return 1; } finally { return 2; } return?Short answer: 2. The finally block always runs after the try block's return value is evaluated, and its own return overrides it. Compilers and linters warn about this pattern (-Xlint:finally, Sonar rule S1143), because it also swallows exceptions.
Short answer: When try-with-resources closes a resource after the body has already thrown, and close() also throws, the close exception is not allowed to hide the original. It's attached to the primary exception with Throwable.addSuppressed(), and available through getSuppressed(). Stack traces print these as Suppressed: ....
try (var conn = dataSource.getConnection()) {
throw new SQLException("query failed"); // primary
} // if conn.close() also fails, its exception is added to the primary's suppressed list
catch (SQLException e) {
for (Throwable s : e.getSuppressed()) log.warn("close also failed", s);
}
Key points to cover:
addSuppressed yourself when aggregating failures, for example cleaning up several resources.Short answer: In two cases:
catch (Exception e) { log(e); throw e; } compiles without declaring throws Exception, as long as e is effectively final. The compiler knows the try block can only throw the checked types it actually throws (plus unchecked exceptions), and checks against those.@SneakyThrows). It's legal bytecode, but it defeats checked-exception contracts. Callers can't catch it by type without a compile error. Avoid it in library APIs.void process() throws IOException { // only IOException needs declaring
try { readFile(); } // throws IOException
catch (Exception e) { audit(e); throw e; } // precise rethrow: compiles
}
Short answer:
new OrderProcessingException("order " + id, e)). Use it to:
SQLException leaking out of a repository API);Common trap: wrapping without passing the cause (new MyException(e.getMessage())) loses the original stack trace. Always pass e as the cause. And don't log and rethrow at every layer, or the same error appears ten times in the logs.
Short answer: Yes. GC is based on reachability, not on lexical scope. Once the JIT determines that a local variable is no longer used later in the method, the object it references can be collected, even though the variable is still "in scope". This can surprise code that relies on finalizers or cleaners running "after" a method is done. Reference.reachabilityFence(obj) (Java 9+) keeps an object reachable up to a point.
String s1 = "abc"; String s2 = "ab" + "c"; System.out.println(s1 == s2); print?Short answer: true. "ab" + "c" is a compile-time constant expression, so the compiler folds it into the literal "abc", which is interned in the same pool entry as s1.
Key points to cover:
String a = "ab"; String s3 = a + "c";), concatenation happens at runtime, and creates a new object, so s1 == s3 is false.final String a = "ab"; it's constant again, so true.Short answer:
free()" leaks. A Java memory leak means objects that are no longer needed but still reachable, so the GC can't reclaim them, and memory grows without bound over time. For example:
Map cache;ThreadLocals in pooled threads;In heap analysis, you look at the retained size and GC root paths (Eclipse MAT's dominator tree) to tell a bounded cache from a leak.
Learn it in depth → Memory Leaks in Java
System.gc() guarantee garbage collection?Short answer: No. It's a request that the JVM may ignore:
-XX:+DisableExplicitGC turns it into a no-op.-XX:+ExplicitGCInvokesConcurrent makes it start a concurrent cycle.By default, in HotSpot, it usually triggers a full, stop-the-world GC. That's why calling it in production code (or libraries doing so, like old RMI DGC calls) causes latency spikes. For tests or diagnostics, use jcmd <pid> GC.run, or heap-dump tools.
finalize() do, and when is it called?Short answer: Object.finalize() was meant as a cleanup hook, called by a finalizer thread at some unspecified time after an object becomes unreachable, possibly never. It has serious problems:
It has been deprecated since Java 9, and deprecated for removal since Java 18 (JEP 421). Use try-with-resources/AutoCloseable for deterministic cleanup, and java.lang.ref.Cleaner as a safety net for native resources.
public final class NativeBuffer implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public NativeBuffer(long size) {
long address = allocate(size);
this.cleanable = CLEANER.register(this, () -> free(address)); // the action must not capture 'this'
}
@Override public void close() { cleanable.clean(); } // deterministic path
}
Short answer: Escape analysis is a C2 JIT analysis that checks whether an object escapes the method or thread that created it (returned, stored in a field, or passed to unknown code). If it doesn't escape, HotSpot can:
synchronized on a non-escaping object is removed).It works best on small, short-lived objects: iterators, builders, Optionals, boxed values in tight loops. Inlining matters, because escape analysis only sees what's inlined. You can diagnose it with -XX:+PrintEscapeAnalysis (debug builds), or indirectly with allocation profiling (JFR).
Short answer: Not while the reference exists. Static fields belong to the class, and a loaded class is reachable through its ClassLoader. So the object becomes collectable only if:
null; orThat's why "static collections that only grow" are the most common leak.
Short answer:
-Xms/-Xmx.-Xss per thread);ByteBuffers and memory-mapped files;malloc calls.Key points to cover:
-XX:NativeMemoryTracking=summary, then jcmd <pid> VM.native_memory) to see the breakdown, and size containers for it.OutOfMemoryError: Metaspace?Short answer: The JVM couldn't allocate space for class metadata, because Metaspace hit -XX:MaxMetaspaceSize (unbounded by default, so it's limited only by native memory) or the compressed class space. Typical causes:
ThreadLocals, JDBC drivers or static caches.How to diagnose:
jcmd <pid> VM.metaspace and VM.classloader_stats;-Xlog:class+load for load and unload patterns.final variable be initialised in a constructor?Short answer: Yes. A blank final instance field (declared final, but not initialised at declaration) must be assigned exactly once on every constructor path, or in an instance initialiser. Otherwise it's a compile error. A static final blank field must be assigned in a static initialiser.
public final class Money {
private final long minorUnits;
private final Currency currency;
public Money(long minorUnits, Currency currency) {
this.minorUnits = minorUnits; // assigned once
this.currency = Objects.requireNonNull(currency);
}
}
Key points to cover:
this), every thread sees their correctly initialised values, without synchronisation. That's the foundation of safe immutable objects.volatile and synchronized?Short answer:
volatile | synchronized | |
|---|---|---|
| Visibility | ✅ reads see the latest writes | ✅ (on entering and exiting the monitor) |
| Ordering | ✅ a happens-before edge on each write → read | ✅ |
| Atomicity | Only single reads and writes (including long/double) | ✅ the whole block is atomic |
| Mutual exclusion | ❌ | ✅ one thread at a time per monitor |
| Blocking | Never blocks | Can block, and deadlock |
| Typical use | Flags, publishing immutable objects, double-checked locking | Compound actions: check-then-act, read-modify-write, invariants over several fields |
volatile int count; count++ is not thread-safe. Use AtomicInteger or LongAdder, or a lock.
Learn it in depth → volatile and the Memory Model
Short answer: Yes.
new: after the superclass constructor (super(...)) returns, and before the rest of the constructor body, in textual order.Across a hierarchy, new Child() runs:
Parent static init, then Child static init (first use only);Parent instance init, then the Parent constructor body;Child instance init, then the Child constructor body.Common trap: calling an overridable method from a superclass constructor. The subclass override runs before the subclass's fields are initialised, so it sees null or 0.
this be used in a static method? Why not?Short answer: No, it's a compile error. A static method belongs to the class, and runs without any instance, so there's no this (and no super). To use instance state, pass an instance as a parameter, or make the method an instance method.
strictfp keyword do?Short answer: Historically, strictfp forced floating-point calculations in a class or method to use strict IEEE 754 semantics, so results were identical on every platform. Without it, x87-era JVMs could use extended precision for intermediate results. It applied to classes, interfaces and methods, but not to variables. Since Java 17 (JEP 306), all floating-point arithmetic is strict, so the keyword is redundant. The compiler warns that it's unnecessary.
Common trap: describing strictfp as something you still need for consistent results. You don't, on Java 17 and later.
Short answer: With no modifier, a class, field, method or constructor is package-private. It's accessible only from code in the same package. Subclasses in other packages can't see it, unlike protected. Visibility, from widest: public > protected (package + subclasses) > package-private > private.
Key points to cover:
public (fields are public static final), unless they're declared private (Java 9+).return in a finally block?Short answer:
try or catch.try or catch, including Errors like OutOfMemoryError.Callers never learn that the operation failed. Never return from finally. Use it only for cleanup that can't throw, or use try-with-resources.
Q: What's printed by try { System.exit(0); } finally { System.out.println("finally"); }?
A: Nothing. System.exit halts the JVM (after shutdown hooks), so finally doesn't run. It also won't run if the thread is killed, or the process crashes.
Q: What is a multi-catch, and what restriction does it have?
A: catch (IOException | SQLException e) handles several unrelated types in one block. The types can't be subclasses of each other, and e is implicitly final.
Q: How do you find which object is holding memory in a leak?
A: Capture a heap dump (jcmd <pid> GC.heap_dump, or -XX:+HeapDumpOnOutOfMemoryError), open it in Eclipse MAT or VisualVM, check the dominator tree and leak suspects, then follow the path to GC roots (excluding weak references) to find the static field, thread or cache keeping it alive.
Q: Why are objects with finalizers slower to allocate and collect?
A: The JVM must register each one in a finalizer queue at allocation. On collection, it must first run finalize() on the finalizer thread, and only reclaim the memory in a later GC cycle. That throttles throughput, and can cause OOMs if finalization falls behind.