JVM.Internals.How does the JVM handle exceptions internally?

💥 What Happens When an Exception is Thrown?

When your code executes something like:

throw new IllegalArgumentException("Bad argument");

The JVM does several things:

1️⃣ Creating the Exception Object

  • A new instance of IllegalArgumentException is created using new.
  • The constructor initializes the stack trace (this is why you get the full trace when printing an exception).

2️⃣ Unwinding the Stack (a.k.a. Stack Unwinding)

  • The JVM immediately looks for a catch block in the current method (the method that threw the exception).
  • If no appropriate catch is found in the method, the method exits (pops off the stack), and the exception propagates to the caller method.
  • This repeats until either:
    • A catch block is found that handles this exception type, or
    • The main method exits and the exception reaches the JVM itself.

3️⃣ Exception Table Lookup

Each method’s bytecode includes an exception table. This table maps:

  • Code ranges (start/stop of a try block)
  • The exception types each catch block can handle
  • The bytecode offset for each corresponding catch block

Example (simplified table):

StartEndHandler PCException Type
1030100java/lang/NullPointerException
1030120java/lang/Exception

The JVM uses this table to decide where control flow should jump when an exception is thrown.


4️⃣ Propagation

If no handler exists in the current method, the method:

  • Cleans up local variables (local variable table is discarded)
  • Releases locks held by synchronized blocks (this is crucial for avoiding deadlocks)
  • Pops off the stack
  • Passes the exception to the caller method (next stack frame)

This continues up the call stack until:

  • A handler (catch) is found, or
  • The stack is fully unwound — if so, the exception is printed to stderr (what you see as the “stack trace”) and the program exits with a non-zero status.

5️⃣ Printing Stack Trace

  • When you call printStackTrace(), the JVM actually uses the stack trace captured when the exception was created (part of Throwable).
  • This is why new Exception() has a performance cost — it captures the whole stack at the point of creation.

📋 Quick Diagram — JVM Exception Flow





+----------------+
| Method X calls Y |
+----------------+
         |
         v
+-----------------+
| Method Y throws |
| NullPointerException |
+-----------------+
         |
         v
+---------------------+
| JVM checks Y's exception table |
+---------------------+
         |
         v
+------------------------------------+
| No catch found — unwind stack frame |
+------------------------------------+
         |
         v
+--------------------+
| Control returns to X |
+--------------------+
         |
         v
+---------------------------+
| X has a catch (maybe) or exits |
+---------------------------+
         |
         v
+--------------------+
| Stack fully unwinds |
+--------------------+
         |
         v
+----------------------------------+
| JVM prints stack trace and exits |
+----------------------------------+

⚠️ Special Case: finally

  • finally blocks always run (even if an exception occurs).
  • When exceptions are thrown, the JVM inserts synthetic bytecode to ensure finally runs before the stack unwinds.
  • You can see this in bytecode dumps — finally blocks often appear as duplicated blocks of code at the end of try blocks.

🛠️ Tools to See It in Action

If you want to observe this:

  • Use javap -c YourClass to see the bytecode (including exception tables).
  • Run with -XX:+TraceExceptions (depending on the JVM, this can show internal exception tracing).

🚀 In Short

StepAction
1Exception object created
2Stack unwinds, finding handlers using the exception table
3finally blocks always run
4If no handler found, exception reaches top-level and JVM exits
5Stack trace printed (captured at creation)
This entry was posted in Без рубрики. Bookmark the permalink.