When a Java statemant throws, the magic does not happen in the source you wrote but in the Exception table that the compiler quietly appends to the method’s bytecode. Let’s open the hood and watch the gears turn.
1 A minimal throw
public static void demo() {
int x = 1 / 0;
}
Compiling and disassembling:
$ javac Demo.java
$ javap -v Demo
Among the output we see only four instructions:
0: iconst_1
1: iconst_0
2: idiv
3: istore_1
4: return
No exception table entry exists, so the ArithmeticException bubbles up uncaught and terminates the thread.
2 Adding a catch
public static void demo() {
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
ex.printStackTrace();
}
}
The bytecode is now longer and contains:
Exception table:
from to target type
0 4 7 Class java/lang/ArithmeticException
fromandtodelimit the protected range[0,4)(note the exclusive upper bound).targetis the index of the first instruction inside thecatchblock.typeis the class the handler accepts.
If the exception matches, control jumps to offset 7; otherwise the JVM re-throws to the caller.
2.1 Why the upper bound is exclusive
The JVM spec admits this is a historical mistake: with a 16-bit code length limit of 65535, an instruction at the very end could not be covered. Compilers therefore cap method size at 65534 bytes.
3 Finally demystified
try {
int x = 1 / 0;
} catch (ArithmeticException ex) {
ex.printStackTrace();
} finally {
System.out.println("cleanup");
}
The exception table grows to three rows:
from to target type
0 4 15 Class java/lang/ArithmeticException
0 4 31 any
15 20 31 any
The two any entries cover every path—normal or exceptional—into the finally block. The compiler duplicates the finally code at every exit point, then wires the table so the block always runs.
3.1 Returning inside finally
try {
int x = 1 / 0;
} finally {
return; // BAD IDEA
}
The bytecode ends with return instead of athrow, swallowing the exception entirely.
4 Where uncaught messages come from
If no handler matches, the JVM eventually lands in
java.lang.Thread.dispatchUncaughtException(Throwable e)
wich delegates to the thread’s ThreadGroup or a custom UncaughtExceptionHandler. The default implementation prints the familiar stack trace to System.err.
5 When an outer catch is blind
try {
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> { int z = 1 / 0; });
} catch (Exception e) {
e.printStackTrace(); // never reached
}
The task runs on a worker thread, so the exception is handled by that thread’s uncaught-handler, not the submitter’s catch. Switching to execute instead of submit surfaces the exception because execute propagates it immediately.
In short, the JVM’s Exception table is the invisible switchboard that decides where control goes when things go wrong. Source code never tells the full story; the bytecode does.