Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Exceptions & Memory
JAVA

Exceptions & Memory

Practice 10 questions covering exception handling, memory management, garbage collection, try-with-resources, and JVM behavior.

1. Which statement accurately describes the memory region Metaspace in Java 8+?

Answer: Metaspace lives in native memory outside the JVM heap and expands dynamically by default, up to available system memory.

Before Java 8, class metadata — the information about your classes, methods, and fields that the JVM needs to run — was stored in a heap region called PermGen. PermGen had a fixed default maximum size, and the classic failure mode was OutOfMemoryError: PermGen space, when an application loaded more classes than that fixed cap allowed.

Java 8 replaced PermGen with Metaspace. The important differences are where it lives and how it grows:

  • Metaspace uses native memory (off-heap), not the JVM heap. User-created object instances still live on the heap; class metadata now lives outside it.
  • By default, Metaspace has no fixed maximum — it grows dynamically as needed, bounded only by the available system memory. That eliminates the PermGen-out-of-space failure for ordinary applications.

The fact that Metaspace is native memory also matters for sizing. Because it is off-heap, it does not compete with the heap for the same space — a large heap and a large Metaspace can coexist. If you do want to cap it, you control it explicitly with flags like -XX:MaxMetaspaceSize.

For interviews, the key points: PermGen → Metaspace in Java 8, Metaspace is off-heap native memory, and it grows dynamically rather than hitting a fixed default cap.

Answer:

Metaspace lives in native memory outside the JVM heap and expands dynamically by default, up to available system memory.

Before Java 8, class metadata — the information about your classes, methods, and fields that the JVM needs to run — was stored in a heap region called PermGen. PermGen had a fixed default maximum size, and the classic failure mode was OutOfMemoryError: PermGen space, when an application loaded more classes than that fixed cap allowed.

Java 8 replaced PermGen with Metaspace. The important differences are where it lives and how it grows:

  • Metaspace uses native memory (off-heap), not the JVM heap. User-created object instances still live on the heap; class metadata now lives outside it.
  • By default, Metaspace has no fixed maximum — it grows dynamically as needed, bounded only by the available system memory. That eliminates the PermGen-out-of-space failure for ordinary applications.

The fact that Metaspace is native memory also matters for sizing. Because it is off-heap, it does not compete with the heap for the same space — a large heap and a large Metaspace can coexist. If you do want to cap it, you control it explicitly with flags like -XX:MaxMetaspaceSize.

For interviews, the key points: PermGen → Metaspace in Java 8, Metaspace is off-heap native memory, and it grows dynamically rather than hitting a fixed default cap.

2. Which garbage collector introduced in recent JDKs aims for sub-millisecond maximum pause times on massive heaps?

Answer: ZGC (the Z Garbage Collector).

ZGC is Java’s low-latency garbage collector, designed around one headline goal: keep pause times predictable and below one millisecond, even on enormous heaps spanning from megabytes to terabytes.

The problem with earlier collectors is that they pause application threads to do their work. Even well-tuned collectors like G1 produce pauses that grow as the heap grows, and a multi-second stop-the-world pause on a huge heap is unacceptable for latency-sensitive services.

ZGC attacks this by doing nearly all of its work concurrently — while application threads keep running. It uses techniques like colored pointers and load barriers, which let it mark, relocate, and remap objects without stopping the world for meaningful amounts of time. The result is that pause time stays tiny and roughly constant, regardless of heap size, instead of scaling with it.

For comparison, CMS was the older concurrent collector but has been removed in modern JDKs. ZGC is the modern answer for the same niche: big heaps plus strict latency requirements.

The interview takeaway: when you see “sub-millisecond pauses” and “massive heaps,” the answer is ZGC — the JDK’s concurrency-first, ultra-low-latency collector.

Answer:

ZGC (the Z Garbage Collector).

ZGC is Java’s low-latency garbage collector, designed around one headline goal: keep pause times predictable and below one millisecond, even on enormous heaps spanning from megabytes to terabytes.

The problem with earlier collectors is that they pause application threads to do their work. Even well-tuned collectors like G1 produce pauses that grow as the heap grows, and a multi-second stop-the-world pause on a huge heap is unacceptable for latency-sensitive services.

ZGC attacks this by doing nearly all of its work concurrently — while application threads keep running. It uses techniques like colored pointers and load barriers, which let it mark, relocate, and remap objects without stopping the world for meaningful amounts of time. The result is that pause time stays tiny and roughly constant, regardless of heap size, instead of scaling with it.

For comparison, CMS was the older concurrent collector but has been removed in modern JDKs. ZGC is the modern answer for the same niche: big heaps plus strict latency requirements.

The interview takeaway: when you see “sub-millisecond pauses” and “massive heaps,” the answer is ZGC — the JDK’s concurrency-first, ultra-low-latency collector.

3. What happens when executing System.gc() in Java code?

Answer: System.gc() is only a hint. It suggests to the JVM that garbage collection might be useful — but the JVM is free to ignore it.

The name is misleading. It sounds like a command, but it is a request. The JVM decides whether, when, and how thoroughly to run a collection. There is no guarantee that a System.gc() call performs immediate or complete collection, and no guarantee that unreferenced objects are reclaimed before the call returns.

Why doesn’t the JVM just obey? Because garbage collection has a cost. A full collection pauses application threads. The JVM’s own heuristics are tuned to minimize those pauses, and it has far better information about when collection is genuinely needed than a single line of application code. Forcing collections on demand would fight the collector’s scheduling and hurt performance.

In modern JDKs, System.gc() is largely a relic. The JVM handles collection automatically. When people call it, it is usually a sign of tuning by guesswork rather than by measurement — and the classic advice is: don’t call it in production code. If you genuinely need to influence collection, prefer JVM flags.

The interview answer: System.gc() is a non-binding hint. It does not force anything. The JVM decides.

Answer:

System.gc() is only a hint. It suggests to the JVM that garbage collection might be useful — but the JVM is free to ignore it.

The name is misleading. It sounds like a command, but it is a request. The JVM decides whether, when, and how thoroughly to run a collection. There is no guarantee that a System.gc() call performs immediate or complete collection, and no guarantee that unreferenced objects are reclaimed before the call returns.

Why doesn’t the JVM just obey? Because garbage collection has a cost. A full collection pauses application threads. The JVM’s own heuristics are tuned to minimize those pauses, and it has far better information about when collection is genuinely needed than a single line of application code. Forcing collections on demand would fight the collector’s scheduling and hurt performance.

In modern JDKs, System.gc() is largely a relic. The JVM handles collection automatically. When people call it, it is usually a sign of tuning by guesswork rather than by measurement — and the classic advice is: don’t call it in production code. If you genuinely need to influence collection, prefer JVM flags.

The interview answer: System.gc() is a non-binding hint. It does not force anything. The JVM decides.

4. What is the outcome of compiling and running this catch block hierarchy?

try {
    throw new ArithmeticException();
} catch (RuntimeException e) {
    System.out.print("RuntimeException ");
} catch (Exception e) {
    System.out.print("Exception ");
}

Output: RuntimeException

When an exception is thrown, Java walks the catch blocks from top to bottom and executes the first block whose parameter type can hold the thrown exception.

ArithmeticException extends RuntimeException. So the first catch (RuntimeException e) matches immediately — it can hold an ArithmeticException because of inheritance. Control enters that block, prints RuntimeException , and the whole try-catch is done. The second catch (Exception e) is never considered.

Order matters, and that’s exactly what the next question exploits.

Answer:

RuntimeException

When an exception is thrown, Java walks the catch blocks from top to bottom and executes the first block whose parameter type can hold the thrown exception.

ArithmeticException extends RuntimeException. So the first catch (RuntimeException e) matches immediately — it can hold an ArithmeticException because of inheritance. Control enters that block, prints RuntimeException , and the whole try-catch is done. The second catch (Exception e) is never considered.

Order matters, and that’s exactly what the next question exploits.

5. What happens if you swap the catch blocks so catch (Exception e) comes FIRST?

try {
    throw new ArithmeticException();
} catch (Exception e) {
    System.out.print("Exception ");
} catch (RuntimeException e) {
    System.out.print("RuntimeException ");
}

Answer: Compilation error: the second catch block is unreachable.

When the broader catch (Exception e) appears first, it can hold every checked and unchecked exception, including ArithmeticException. Any exception thrown in the try block would be caught there. Java’s rules require that a catch block only be allowed if there is still some exception it could catch.

The subsequent catch (RuntimeException e) block could never be reached — everything it could catch is already handled by the first block. So the compiler flags it as an unreachable catch block and refuses to compile the code.

The rule to remember: catch blocks are matched top-to-bottom, so the most specific catch must always come first, and broader catches go after. catch (Exception) swallowing everything is why you list RuntimeException (and any other specific types) before it.

Answer:

Compilation error: the second catch block is unreachable.

When the broader catch (Exception e) appears first, it can hold every checked and unchecked exception, including ArithmeticException. Any exception thrown in the try block would be caught there. Java’s rules require that a catch block only be allowed if there is still some exception it could catch.

The subsequent catch (RuntimeException e) block could never be reached — everything it could catch is already handled by the first block. So the compiler flags it as an unreachable catch block and refuses to compile the code.

The rule to remember: catch blocks are matched top-to-bottom, so the most specific catch must always come first, and broader catches go after. catch (Exception) swallowing everything is why you list RuntimeException (and any other specific types) before it.

6. What does the transient keyword do when applied to a class field?

Answer: It excludes the field from standard Java serialization — the field’s value is skipped when the object is written out and restored as default when read back.

Serialization is the mechanism by which an object’s state is written to a byte stream (ObjectOutputStream) and reconstructed later (ObjectInputStream). By default, every non-static, non-transient field is included. The transient keyword opts a field out.

When is that useful? Some fields simply shouldn’t survive serialization:

  • Derived or cached values — a field recomputed from others, like a checksum or a lazily-initialized cache. Saving it is wasted space and risks saving stale data.
  • Sensitive data — passwords, tokens, keys. If the object might be serialized, marking these transient keeps them out of the stream.
  • Non-serializable resources — a field holding a Socket, a Thread, or a JDBC connection cannot be meaningfully written to bytes. Marking it transient is practically required.

When the object is deserialized, a transient field is simply left at its default value (null for objects, 0 for primitives, false for booleans). Your code is responsible for reconstructing it if needed — often in a readObject method.

The interview answer: transient marks a field to be skipped during serialization; it’s restored to its default value after deserialization.

Answer:

It excludes the field from standard Java serialization — the field’s value is skipped when the object is written out and restored as default when read back.

Serialization is the mechanism by which an object’s state is written to a byte stream (ObjectOutputStream) and reconstructed later (ObjectInputStream). By default, every non-static, non-transient field is included. The transient keyword opts a field out.

When is that useful? Some fields simply shouldn’t survive serialization:

  • Derived or cached values — a field recomputed from others, like a checksum or a lazily-initialized cache. Saving it is wasted space and risks saving stale data.
  • Sensitive data — passwords, tokens, keys. If the object might be serialized, marking these transient keeps them out of the stream.
  • Non-serializable resources — a field holding a Socket, a Thread, or a JDBC connection cannot be meaningfully written to bytes. Marking it transient is practically required.

When the object is deserialized, a transient field is simply left at its default value (null for objects, 0 for primitives, false for booleans). Your code is responsible for reconstructing it if needed — often in a readObject method.

The interview answer: transient marks a field to be skipped during serialization; it’s restored to its default value after deserialization.

7. What happens if an unhandled checked exception is thrown inside Runnable.run()?

Answer: Compilation error — run() does not declare a throws clause, so checked exceptions must be handled inside it.

This is one of the constraints that shapes how Runnable is used. The interface’s single abstract method is void run(), and its signature includes no throws declaration. When you override run(), you cannot widen that contract: any checked exception thrown by the code inside must be caught or declared within the method — but you can’t declare it on the method itself.

So a checked exception inside run() forces a try/catch (or a wrapping in an unchecked exception). Unchecked exceptions (RuntimeException and its subclasses) are fine — they don’t need to be declared, and an uncaught one will propagate to the thread’s uncaught exception handler.

The practical consequence: Runnable is awkward for code that throws checked exceptions like IOException or InterruptedException. That’s one reason Callable<V> exists — its call() method does declare throws Exception, making it the right tool when you need to propagate checked failures.

The interview answer: Runnable.run() throws nothing, so checked exceptions inside it must be caught locally or wrapped in an unchecked exception — otherwise the code won’t compile.

Answer:

Compilation error — run() does not declare a throws clause, so checked exceptions must be handled inside it.

This is one of the constraints that shapes how Runnable is used. The interface’s single abstract method is void run(), and its signature includes no throws declaration. When you override run(), you cannot widen that contract: any checked exception thrown by the code inside must be caught or declared within the method — but you can’t declare it on the method itself.

So a checked exception inside run() forces a try/catch (or a wrapping in an unchecked exception). Unchecked exceptions (RuntimeException and its subclasses) are fine — they don’t need to be declared, and an uncaught one will propagate to the thread’s uncaught exception handler.

The practical consequence: Runnable is awkward for code that throws checked exceptions like IOException or InterruptedException. That’s one reason Callable<V> exists — its call() method does declare throws Exception, making it the right tool when you need to propagate checked failures.

The interview answer: Runnable.run() throws nothing, so checked exceptions inside it must be caught locally or wrapped in an unchecked exception — otherwise the code won’t compile.

8. What happens if an Optional contains null and Optional.of(null) is invoked?

Answer: Optional.of(null) throws NullPointerException immediately.

Optional is meant to represent a value that might be absent. But there’s an important asymmetry in its factory methods:

  • Optional.of(value) requires a non-null value. Pass null and it throws NullPointerException right away. It asserts “this definitely has a value.”
  • Optional.ofNullable(value) accepts null, returning Optional.empty() for it. It is the “maybe” variant.

There is no Optional in existence that contains null as a present value — a present Optional always holds a non-null object. So Optional.of(null) can never produce a useful result; it fails fast.

The rule to remember: if there’s any chance the value can be null, use ofNullable. Use of only when you are certain the value is present — it doubles as a null-check. The interview answer is simply: NullPointerException, because Optional.of rejects null.

Answer:

Optional.of(null) throws NullPointerException immediately.

Optional is meant to represent a value that might be absent. But there’s an important asymmetry in its factory methods:

  • Optional.of(value) requires a non-null value. Pass null and it throws NullPointerException right away. It asserts “this definitely has a value.”
  • Optional.ofNullable(value) accepts null, returning Optional.empty() for it. It is the “maybe” variant.

There is no Optional in existence that contains null as a present value — a present Optional always holds a non-null object. So Optional.of(null) can never produce a useful result; it fails fast.

The rule to remember: if there’s any chance the value can be null, use ofNullable. Use of only when you are certain the value is present — it doubles as a null-check. The interview answer is simply: NullPointerException, because Optional.of rejects null.

9. Which classloader in the JVM hierarchy is responsible for loading standard runtime classes like java.lang.Object?

Answer: The Bootstrap ClassLoader.

The JVM loads classes through a hierarchy of classloaders, each with a defined responsibility:

  • Bootstrap ClassLoader — the root of the hierarchy. It’s not a Java class at all; it’s implemented natively (in C/C++) and is built into the JVM. It loads the core JDK classes — everything in java.base, which includes java.lang.Object, String, the collections, and the rest of the fundamental runtime.
  • Platform (Extension) ClassLoader — loads JDK modules beyond the core, historically the extension and module-path classes.
  • Application (System) ClassLoader — loads classes from the application’s own classpath.
  • Custom ClassLoaders — user-defined loaders for specialized needs like hot-reloading or plugin systems.

The point to remember: the most fundamental classes, the ones the JVM needs before anything else works, come from the Bootstrap ClassLoader. It sits at the base of the hierarchy and delegates nothing upward because it’s already the top.

Answer:

The Bootstrap ClassLoader.

The JVM loads classes through a hierarchy of classloaders, each with a defined responsibility:

  • Bootstrap ClassLoader — the root of the hierarchy. It’s not a Java class at all; it’s implemented natively (in C/C++) and is built into the JVM. It loads the core JDK classes — everything in java.base, which includes java.lang.Object, String, the collections, and the rest of the fundamental runtime.
  • Platform (Extension) ClassLoader — loads JDK modules beyond the core, historically the extension and module-path classes.
  • Application (System) ClassLoader — loads classes from the application’s own classpath.
  • Custom ClassLoaders — user-defined loaders for specialized needs like hot-reloading or plugin systems.

The point to remember: the most fundamental classes, the ones the JVM needs before anything else works, come from the Bootstrap ClassLoader. It sits at the base of the hierarchy and delegates nothing upward because it’s already the top.

10. What is the output of this exception handling block?

public class ThrowTest {
    public static void main(String[] args) {
        try {
            throw null;
        } catch (Exception e) {
            System.out.println(e.getClass().getSimpleName());
        }
    }
}

Output: NullPointerException

You can write throw null — it compiles. The question is what happens when it executes.

In Java, the throw statement requires a Throwable. When you throw null, there is no actual exception object to throw. The JVM resolves this at runtime by replacing the null with a freshly created NullPointerException.

That exception propagates up and is caught by catch (Exception e). NullPointerException is a subclass of Exception, so the catch matches. e.getClass().getSimpleName() prints NullPointerException.

The interview point: throw null compiles and, when executed, throws a NullPointerException. It’s a deliberately confusing question that tests whether you know the JVM’s behavior rather than just the grammar.

Answer:

NullPointerException

You can write throw null — it compiles. The question is what happens when it executes.

In Java, the throw statement requires a Throwable. When you throw null, there is no actual exception object to throw. The JVM resolves this at runtime by replacing the null with a freshly created NullPointerException.

That exception propagates up and is caught by catch (Exception e). NullPointerException is a subclass of Exception, so the catch matches. e.getClass().getSimpleName() prints NullPointerException.

The interview point: throw null compiles and, when executed, throws a NullPointerException. It’s a deliberately confusing question that tests whether you know the JVM’s behavior rather than just the grammar.

My Private Notes

Notes are auto-saved locally to this device.