Lỗi gặp phải
Bạn chạy ứng dụng Java và JVM ném ra:
Exception in thread "main" java.lang.StackOverflowError
at com.example.MyClass.calculate(MyClass.java:12)
at com.example.MyClass.calculate(MyClass.java:12)
at com.example.MyClass.calculate(MyClass.java:12)
... (lặp lại hàng trăm lần)
Cùng một dòng, lặp đi lặp lại hàng trăm lần. Đó là dấu hiệu rõ ràng — JVM đã dùng hết không gian call stack trước khi code của bạn chạy xong.
Tại sao lỗi này xảy ra
Mỗi lần gọi hàm trong Java sẽ tạo ra một stack frame riêng. JVM dành sẵn một vùng bộ nhớ cố định cho mỗi thread — thường từ 512KB đến 1MB. Nếu các lời gọi lồng nhau quá sâu mà không được giải phóng, vùng nhớ đó sẽ đầy rất nhanh.
Ba tình huống thường gây ra lỗi này nhất:
- Thiếu hoặc sai điều kiện dừng trong hàm đệ quy
- Đệ quy chéo — hàm A gọi hàm B, hàm B lại gọi lại hàm A
- Tự gọi chính mình vô tình — một getter tự gọi lại chính nó, hoặc một
toString()kích hoạt mộttoString()khác
Cách sửa 1 — Sửa điều kiện dừng
Tìm dòng bị lặp lại trong stack trace, mở hàm đó và kiểm tra xem nó có điều kiện dừng hay không.
Code bị lỗi:
public int factorial(int n) {
return n * factorial(n - 1); // Không có điều kiện dừng — chạy mãi mãi
}
Sau khi sửa:
public int factorial(int n) {
if (n stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.println(node.val);
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
}
Cách sửa 3 — Tìm các trường hợp tự gọi vô tình
Đây là lỗi hay gặp mà nhiều người không để ý. Một toString() tham chiếu chính nó thay vì một field sẽ lặp vô tận:
// Lỗi: toString tự gọi chính mình
@Override
public String toString() {
return "User: " + toString(); // Phải là this.name, không phải gọi method
}
// Sửa:
@Override
public String toString() {
return "User: " + this.name;
}
Cũng cần chú ý các hàm equals()/hashCode() được sinh tự động bởi Lombok hoặc IDE trên các entity có tham chiếu vòng — điều này thường xảy ra trong quan hệ JPA hai chiều. Dùng annotation @ToString.Exclude hoặc @EqualsAndHashCode.Exclude cho field tham chiếu ngược để phá vỡ vòng lặp.
Cách sửa 4 — Tăng kích thước stack (Phương án cuối cùng)
Chưa thể refactor ngay? Bạn có thể dùng flag -Xss để tạm thời xử lý. Mặc định thường là 512k hoặc 1m — thử tăng gấp bốn:
java -Xss4m -jar your-app.jar
Cần stack lớn hơn cho một thread cụ thể? Thiết lập trong constructor của Thread:
Thread thread = new Thread(null, () -> {
runDeepRecursion();
}, "deep-thread", 4 * 1024 * 1024); // Stack 4MB
thread.start();
Cách này không giải quyết gốc rễ vấn đề. Với đầu vào đủ lớn, lỗi vẫn sẽ quay lại. Dùng biện pháp này trong khi chờ sửa triệt để — không phải thay thế cho việc sửa thật sự.
Các bước kiểm tra sau khi sửa
Sau khi đã áp dụng cách sửa, hãy thực hiện các kiểm tra sau:
- Chạy lại đúng đầu vào đã gây crash. Lỗi phải biến mất.
- Kiểm tra các trường hợp biên:
n = 0,n = 1, giá trị âm, danh sách rỗng. - Thử với đầu vào lớn trên phiên bản dùng vòng lặp — thử
n = 100000— để xác nhận vẫn hoạt động tốt. - Viết unit test để tránh lỗi tái phát:
@Test
public void testFactorialLargeInput() {
// Không được ném StackOverflowError
assertDoesNotThrow(() -> factorial(10000));
}
Mẹo phòng tránh
- Viết điều kiện dừng trước tiên, ở đầu hàm, trước bất kỳ lời gọi đệ quy nào.
- Với đầu vào do người dùng kiểm soát, hãy giới hạn độ sâu đệ quy một cách tường minh:
public void traverse(Node node, int depth) {
if (node == null || depth > 1000) return;
traverse(node.next, depth + 1);
}
- Đồ thị và cây có thể có chu trình. Theo dõi các node đã duyệt trong một
Setđể tránh lặp vô tận. - Khi review code, hãy coi mọi hàm đệ quy là rủi ro tiềm ẩn — kiểm tra xem điều kiện dừng có đúng không và liệu nó có thực sự được đạt tới không.

