Exception table và cost của exception — throw rẻ, tạo đắt
Exception table trong bytecode giúp JVM dispatch catch bằng table lookup, không tốn runtime check. Phân tích chi phí fillInStackTrace và tối ưu hot path.
TL;DR: JVM xử lý khối catch thông qua cấu trúc Exception Table được nhúng sẵn trong bytecode của phương thức. Thao tác này là một lượt tra bảng thuần túy (table lookup), không tốn chi phí rẽ nhánh điều kiện lúc chạy, giúp quá trình ném và bắt ngoại lệ chỉ mất khoảng 100 nano-giây. Chi phí thực sự của ngoại lệ nằm ở phương thức native fillInStackTrace() (mất từ 1 đến 10 micro-giây) khi khởi tạo đối tượng Throwable. Đó là lý do Effective Java khuyên không bao giờ dùng ngoại lệ để điều khiển luồng logic thông thường.
Ở bài trước, bạn đã biết cách phân tích chuỗi vết tích stack trace và hiểu rằng JVM gọi hàm native fillInStackTrace() ngay khi khởi tạo ngoại lệ. Bài học này sẽ đi sâu vào tầng Bytecode để giải mã cơ chế tìm kiếm khối catch và định lượng chi phí thực thi của từng thành phần.
Nhiều lập trình viên truyền tai nhau rằng: "Khối try-catch làm chậm chương trình". Thực hư chuyện này ra sao? Việc đặt một khối mã bên trong try-catch có tốn thêm chu kỳ CPU nào không nếu không có lỗi xảy ra? Và khi xảy ra lỗi, phần nào thực sự làm giảm tốc độ thực thi?
1. Cơ chế Exception Table trong Java Bytecode hoạt động ra sao?
Khi bạn viết một khối try-catch, trình biên dịch javac không sinh ra các lệnh so sánh kiểu như if (error instanceof IOException). Thay vào đó, nó tạo ra một bảng tra cứu tĩnh trong bytecode gọi là Exception Table.
Quan sát mã nguồn:
public void processCatalog() {
try {
readDiskFile();
} catch (IOException e) {
handleDiskError(e);
} catch (SQLException e) {
handleDbError(e);
}
}
Khi dịch sang mã bytecode bằng lệnh javap -c -v, JVM tổ chức như sau:
Code:
0: invokestatic #2 // Method readDiskFile:()V
3: goto 19 // Neu khong co loi, nhay thang ve return
Exception table:
from to target type
0 3 6 java/io/IOException
0 3 14 java/sql/SQLException
6: astore_1 // Bat IOException: Luu ngoai le vao bien cuc bo
7: aload_1
8: invokestatic #3 // Method handleDiskError:(Ljava/io/IOException;)V
11: goto 19
14: astore_1 // Bat SQLException: Luu ngoai le vao bien cuc bo
15: aload_1
16: invokestatic #4 // Method handleDbError:(Ljava/sql/SQLException;)V
19: return // Ket thuc phuong thuc
Quy trình tra cứu khi ngoại lệ được ném ra
Khi lệnh athrow được thực thi:
- JVM lấy chỉ số bộ đếm chương trình hiện tại (Program Counter - PC).
- JVM quét bảng Exception Table từ trên xuống dưới.
- Kiểm tra hai điều kiện: PC có nằm trong khoảng
fromđếntohay không, và đối tượng ngoại lệ có thuộc kiểutypehay không. - Nếu khớp: JVM nhảy thẳng tới chỉ số byte code
target(chính là thân khốicatch). Stack frame của phương thức hiện tại không bị loại bỏ, các biến cục bộ được giữ nguyên vẹn. - Nếu không khớp: JVM pop stack frame hiện tại và tiếp tục quét Exception Table của phương thức gọi phía trên.

Kết luận quan trọng: Zero-cost Try Block. Trong trường hợp bình thường (không có ngoại lệ phát sinh), việc đặt mã nguồn trong try hoàn toàn không tốn thêm bất kỳ chi phí kiểm tra điều kiện nào lúc chạy.
2. Chi phí thực sự của ngoại lệ nằm ở đâu?
Thao tác tra bảng Exception Table diễn ra rất nhanh (chỉ khoảng 50 đến 100 nano-giây). Thủ phạm thực sự gây sụt giảm hiệu năng nằm ở hàm fillInStackTrace():
Mỗi khi bạn gọi new Exception():
- Máy ảo phải dừng việc thực thi và đi ngược toàn bộ các khung Call Stack của luồng hiện tại.
- Thu thập metadata từ bảng biểu của lớp (tên lớp, phương thức, file mã nguồn, số dòng).
- Khởi tạo một mảng chứa hàng chục đối tượng
StackTraceElement.
Nếu độ sâu của Call Stack là 30-50 tầng (rất phổ biến trong các framework như Spring Boot), một lần gọi fillInStackTrace() tốn từ 1 đến 10 micro-giây. Nếu một vòng lặp xử lý 1 triệu bản ghi mà lạm dụng ném ngoại lệ, ứng dụng sẽ dành phần lớn thời gian CPU chỉ để duyệt stack trace.
Cơ chế JIT Tối ưu: OmitStackTraceInFastThrow
Để bảo vệ hiệu năng máy ảo, JIT Compiler (C2) tích hợp một cơ chế tối ưu mang tên OmitStackTraceInFastThrow (mặc định được bật):
Khi một ngoại lệ ngầm định của máy ảo (như NullPointerException, ArithmeticException) bị ném lặp đi lặp lại hàng nghìn lần tại cùng một dòng mã, JIT sẽ thay thế nó bằng một thể hiện ngoại lệ được khởi tạo sẵn (pre-allocated) không có thông tin stack trace.
Đó là lý do trên môi trường production, đôi khi bạn thấy log xuất hiện các dòng NullPointerException trống trơn không có danh sách dòng code phía sau. Bạn có thể tắt tính năng này để điều tra bằng cờ JVM: -XX:-OmitStackTraceInFastThrow.
3. Anti-Pattern: Dùng ngoại lệ để điều khiển luồng logic (Control Flow)
Quan sát đoạn mã tìm kiếm ấn phẩm thư viện:
// PHONG CACH TOI: Dung exception thay cho if-else
public LibraryItem findBookBad(List<LibraryItem> books, String id) {
try {
int index = 0;
while (true) {
LibraryItem item = books.get(index++);
if (item.getId().equals(id)) return item;
}
} catch (IndexOutOfBoundsException e) {
return null; // Dung ngoai le de bao hieu het danh sach
}
}
Tại sao đoạn mã này bị coi là tệ hại?
- Chậm hơn từ 50 đến 100 lần: Vòng lặp phải trả giá đắt cho việc khởi tạo ngoại lệ và cuộn stack khi chạm đáy danh sách.
- Vô hiệu hoá tối ưu JIT: JIT Compiler tối ưu hoá các vòng lặp dựa trên giả định luồng chạy tiêu chuẩn. Việc cố tình ném lỗi khiến JIT không thể thực hiện kỹ thuật Loop Unrolling hoặc Vectorization.
- Che giấu lỗi thực tế: Nếu bên trong danh sách có một hàm nào khác vô tình ném
IndexOutOfBoundsException, khốicatchsẽ nuốt chửng lỗi đó và trả vềnull.
Cách viết chuẩn tắc:
public LibraryItem findBookGood(List<LibraryItem> books, String id) {
for (LibraryItem item : books) {
if (item.getId().equals(id)) return item;
}
return null;
}
4. Kỹ thuật tối ưu hóa ngoại lệ trong các hệ thống High-Throughput
Trong các module xử lý dữ liệu tải cao (như Gateway, Parser giao thức tài chính), nếu bắt buộc phải dùng ngoại lệ như một tín hiệu báo ngắt nhanh (Short-circuit Signal), bạn có thể áp dụng hai kỹ thuật sau:
Kỹ thuật 1: Ghi đè hàm fillInStackTrace()
Bằng cách ghi đè phương thức fillInStackTrace() để trả về chính nó mà không làm gì:
public class FastSignalException extends RuntimeException {
@Override
public synchronized Throwable fillInStackTrace() {
return this; // Bo qua hoan toan viec duyet call stack
}
}
Hiệu năng của việc tạo và ném FastSignalException sẽ nhanh ngang bằng với một lệnh tạo đối tượng thông thường.
Kỹ thuật 2: Sử dụng Constructor tắt WritableStackTrace
Từ Java 7, lớp Throwable cung cấp constructor chuyên dụng:
public class RouteNotFoundSignal extends RuntimeException {
public static final RouteNotFoundSignal INSTANCE = new RouteNotFoundSignal();
private RouteNotFoundSignal() {
super(null, null, false, false);
// Tham so thu 3: enableSuppression = false
// Tham so thu 4: writableStackTrace = false (Tat hoan toan stack trace)
}
}
Khi writableStackTrace là false, JVM sẽ không cấp phát bộ nhớ cho mảng stack trace. Kết hợp với việc tái sử dụng một đối tượng hằng số (INSTANCE), chi phí cấp phát bộ nhớ sẽ bằng 0.
5. Java 9 StackWalker: Duyệt Stack hiệu năng cao theo yêu cầu
Trước Java 9, nếu bạn chỉ muốn biết tên phương thức đã gọi mình (Caller Frame), cách duy nhất là gọi Thread.currentThread().getStackTrace(), buộc JVM phải tạo toàn bộ mảng khung stack đồ sộ.
Java 9 giới thiệu StackWalker, cho phép duyệt stack dạng Stream lười (Lazy Evaluation):
public class AuditInspector {
public static String getCallerMethodName() {
return StackWalker.getInstance()
.walk(frames -> frames
.skip(1) // Bo qua chinh ham getCallerMethodName
.findFirst()
.map(StackWalker.StackFrame::getMethodName)
.orElse("UNKNOWN"));
}
}
JVM chỉ phân tích đúng số lượng khung stack cần thiết thay vì duyệt toàn bộ chiều sâu của Call Stack, mang lại hiệu năng vượt trội cho các framework ghi log và kiểm toán.
6. Deep Dive Oracle & OpenJDK References
- JVMS §4.7.3 (The Code Attribute & Exception Table): Đặc tả cấu trúc nhị phân của Exception Table trong file class Java.
- Effective Java (Joshua Bloch) Item 69: "Use exceptions only for exceptional conditions" (Phân tích chi tiết tại sao việc dùng ngoại lệ cho luồng bình thường là tai hoạ hiệu năng).
- JEP 259 (Stack-Walking API): Giới thiệu API StackWalker trong Java 9 để thay thế cơ chế duyệt stack trace truyền thống.
7. Tự kiểm tra kiến thức
- Q1Tại sao khối lệnh try-catch được gọi là 'Zero-cost' khi chương trình chạy bình thường không có lỗi?
- Q2Phần nào trong quá trình xử lý ngoại lệ chiếm phần lớn thời gian CPU?
- Q3Cơ chế OmitStackTraceInFastThrow của máy ảo HotSpot hoạt động như thế nào và gây ra hiện tượng gì trong log?
- Q4Kỹ thuật nào giúp tối ưu hoá ngoại lệ thành một tín hiệu báo ngắt (signal) trong các hệ thống High-Throughput?
- Q5Ưu điểm vượt trội của StackWalker (Java 9) so với Thread.currentThread().getStackTrace() là gì?
Bài tiếp theo: Checked vs Unchecked exception — tranh luận lớn nhất của Java
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Đọc stack trace và cause chain — giữ dấu vết lỗi qua mọi layer
Bài tiếpChecked vs Unchecked — cuộc tranh luận lớn trong thiết kế Java
Lộ trình Junior Java Backend Developer
Chặng 4/21 · Exception handling — bài 3/7 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này