Back to JVM Exception Internals
Q1Question 1 of 2

What is the JVM exception table?

Key Concepts
  • Each method in bytecode has an exception table with entries: start-PC, end-PC, handler-PC, and caught exception type
  • When an exception is thrown, the JVM looks up the table for a matching entry based on PC range and exception type
  • The try block is just normal bytecode with ZERO overhead: no extra branches, no push/pop markers
  • The exception table is metadata, not executable code, so it is only consulted when a throw actually happens
  • If no matching handler is found, the JVM pops the current frame and repeats in the calling method (stack unwinding)
  • This design means try-catch is free on the happy path and the cost is only paid when exceptions are actually thrown

Definition

The JVM uses a per-method exception table in bytecode to find catch blocks, and the try block has zero runtime overhead because the table is metadata only consulted when a throw actually happens.
  • Each compiled method has an exception table in its bytecode: a list of entries [try-start-PC, try-end-PC, handler-PC, caught-type].
  • When an exception is thrown, the JVM checks the table for the current method: finds an entry where (1) current PC is between try-start and try-end, and (2) thrown exception IS-A caught-type.
  • If found, the JVM jumps to handler-PC (the catch block). If not found, it pops the frame and repeats in the calling method (stack unwinding).
  • The try block is just normal bytecode with no extra instructions. No overhead on the happy path. The exception table is metadata, not executable code.

National Bank of India does not post a guard at every door just in case. Instead, there is an emergency manual in the office (exception table): "If fire alarm between 9AM and 5PM (try range), go to Exit B (handler)." The manual sits unused during normal operations (zero cost). Only when the alarm rings (exception thrown) does someone consult it.

Understanding the Concepts

How does the exception table work?

Each method has an exception table in its bytecode. The JVM compiler (javac) generates it from your try-catch structure.

JAVA
package com.bank.demo;

import java.io.IOException;
import java.sql.SQLException;

public class ExceptionTableDemo {

    // This method compiles to bytecode with an exception table
    public static String process(String tradeId) {
        try {
            return settle(tradeId);        // bytecode: PC 0-10
        } catch (IllegalArgumentException e) {
            return "BAD_INPUT";             // handler at PC 13
        } catch (RuntimeException e) {
            return "RUNTIME_ERROR";         // handler at PC 20
        }
    }

    private static String settle(String id) {
        if (id.equals("BAD")) throw new IllegalArgumentException("Invalid trade");
        return "SETTLED: " + id;
    }

    public static void main(String[] args) {
        System.out.println(process("TRD-001"));
        System.out.println(process("BAD"));

        // Exception table (from javap -c -v):
        // from  to  target  type
        //   0   10    13    IllegalArgumentException
        //   0   10    20    RuntimeException
        System.out.println();
        System.out.println("Exception table maps PC ranges to handlers");
        System.out.println("Try body (PC 0-10) is plain bytecode, ZERO overhead");
    }
}
TEXT
SETTLED: TRD-001
BAD_INPUT

Exception table maps PC ranges to handlers
Try body (PC 0-10) is plain bytecode, ZERO overhead

Why does try have zero runtime cost?

The try body compiles to plain bytecode with no extra instructions. No markers, no branches, no registration. The exception table is metadata, never executed on the happy path.

JAVA
package com.bank.demo;

public class ZeroCostTryDemo {

    // With try-catch: SAME bytecode as without try-catch on happy path
    public static int withTryCatch(int a, int b) {
        try {
            return a + b;  // just: iload, iload, iadd, ireturn
        } catch (Exception e) {
            return -1;
        }
    }

    // Without try-catch
    public static int withoutTryCatch(int a, int b) {
        return a + b;  // same: iload, iload, iadd, ireturn
    }

    public static void main(String[] args) {
        int iterations = 1_000_000;

        long start = System.nanoTime();
        for (int i = 0; i < iterations; i++) withTryCatch(i, i);
        long withMs = (System.nanoTime() - start) / 1_000_000;

        start = System.nanoTime();
        for (int i = 0; i < iterations; i++) withoutTryCatch(i, i);
        long withoutMs = (System.nanoTime() - start) / 1_000_000;

        System.out.println("With try-catch: " + withMs + "ms");
        System.out.println("Without try-catch: " + withoutMs + "ms");
        System.out.println("Difference: negligible (try body is the same bytecode)");
    }
}
TEXT
With try-catch: 3ms
Without try-catch: 3ms
Difference: negligible (try body is the same bytecode)

How do you inspect the exception table?

Use javap -c -v to disassemble a class file and see the exception table.

JAVA
package com.bank.demo;

public class JavapDemo {
    public static void main(String[] args) {
        System.out.println("To see the exception table:");
        System.out.println("  javac ExceptionTableDemo.java");
        System.out.println("  javap -c -v ExceptionTableDemo.class");
        System.out.println();
        System.out.println("Output includes:");
        System.out.println("  Exception table:");
        System.out.println("    from  to  target  type");
        System.out.println("      0   10    13    Class IllegalArgumentException");
        System.out.println("      0   10    20    Class RuntimeException");
    }
}
TEXT
To see the exception table:
  javac ExceptionTableDemo.java
  javap -c -v ExceptionTableDemo.class

Output includes:
  Exception table:
    from  to  target  type
      0   10    13    Class IllegalArgumentException
      0   10    20    Class RuntimeException

What should a senior engineer know?

  • finally is an exception table entry too. The finally block compiles to a handler for "any" type (catches all), ensuring cleanup runs.
  • JIT and try-catch. The JIT compiler can still optimize code inside try blocks. The exception table does not prevent inlining or other optimizations on the happy path.
  • Nested try-catch. Each nested try produces additional exception table entries. The JVM searches them in order: innermost first.

What are common interview follow-up questions?

  • Does try-catch slow down normal execution? (No, the try body is plain bytecode with zero overhead.)
  • When is the exception table consulted? (Only when a throw actually happens.)
  • How does the JVM find the right catch? (Matches PC range and exception type IS-A.)
  • What happens if no handler is found? (Frame is popped, search continues in the caller.)

Visual Overview

DIAGRAM
  Method Bytecode:
  ┌──────────────────────────────────┐
  │ PC 0-10: try body (plain code)   │ ← no extra instructions
  │ PC 11:   return                  │
  │ PC 13:   catch IAE handler       │
  │ PC 20:   catch RTE handler       │
  └──────────────────────────────────┘

  Exception Table (metadata):
  ┌──────┬──────┬────────┬──────────────────────┐
  │ from │  to  │ target │ type                  │
  ├──────┼──────┼────────┼──────────────────────┤
  │   0  │  10  │   13   │ IllegalArgumentExc    │
  │   0  │  10  │   20   │ RuntimeException      │
  └──────┴──────┴────────┴──────────────────────┘

  Happy path: table never consulted. ZERO cost.
  Exception thrown: table lookup + stack unwinding.

Key Takeaway

The JVM uses a per-method exception table in the bytecode to find matching catch blocks. Each entry maps a PC range (try body) to a handler PC (catch block) for a specific exception type. When no exception is thrown, the try body executes as plain bytecode with zero overhead. The exception table is metadata, never consulted on the happy path.

The cost appears only when an exception is actually thrown: fillInStackTrace walks the stack (1 to 5 microseconds), the JVM looks up the exception table (fast, small table), and if no match is found, it pops the frame and repeats in the calling method (stack unwinding). This is why try-catch is free but throwing is expensive.

At National Bank of India, Pankaj wraps every database call in try-catch without worrying about performance on the happy path. The try block adds zero overhead. The exception table is only consulted when the database actually throws, which happens rarely compared to successful operations.

Remember one sentence: try-catch has zero runtime cost on the happy path because the exception table is metadata only consulted when a throw actually happens.

---

Share
Use ← → arrow keys to navigate