StackOverflowError trong Java
Người dịch: Nguyễn Thành Trung – Học viên lớp Java08
Email liên hệ:
Bài viết gốc: https://www.baeldung.com/java-stack-overflow-error
1. Tổng quan
StackOverflowError có thể gây khó chịu cho các nhà phát triển Java, vì đây là một trong những lỗi runtime phổ biến nhất mà chúng ta có thể gặp phải.
Trong bài viết này, chúng ta sẽ xem lỗi này có thể xảy ra như thế nào bằng cách xem xét nhiều ví dụ khác nhau cũng như cách xử lý nó.
2. Stack Frames và StackOverflowError xảy ra ra sao
Hãy bắt đầu với những điều cơ bản. Khi một phương thức được gọi, một stack frame mới sẽ được tạo trên call Stack. Stack frame này chứa các tham số của phương thức được gọi, các biến cục bộ của nó và địa chỉ trả về của phương thức, tức là điểm mà từ đó việc thực thi phương thức sẽ tiếp tục sau khi phương thức được gọi đã trả về.
Việc tạo các Stack frame sẽ tiếp tục cho đến khi nó kết thúc các lệnh gọi phương thức được tìm thấy bên trong các phương thức lồng nhau.
Trong quá trình này, nếu JVM gặp phải tình huống không có không gian để tạo stack frame mới, nó sẽ tạo ra lỗi StackOverflowError.
Nguyên nhân phổ biến nhất khiến JVM gặp phải tình huống này là đệ quy không kết thúc / vô hạn – mô tả Javadoc cho StackOverflowError đề cập rằng lỗi được tạo ra do đệ quy quá sâu trong một đoạn mã cụ thể.
Tuy nhiên, đệ quy không phải là nguyên nhân duy nhất gây ra lỗi này. Nó cũng có thể xảy ra trong tình huống ứng dụng tiếp tục gọi các phương thức từ bên trong các phương thức cho đến khi hết ngăn xếp. Đây là một trường hợp hiếm hoi vì không có nhà phát triển nào cố tình làm theo các phương pháp mã hóa xấu. Một nguyên nhân hiếm gặp khác là có một số lượng lớn các biến cục bộ bên trong một phương thức.
StackOverflowError cũng có thể được ném ra khi một ứng dụng được thiết kế để có mối quan hệ tuần hoàn giữa các lớp . Trong tình huống này, các hàm tạo của nhau được gọi lặp đi lặp lại, điều này gây ra lỗi này. Đây cũng có thể coi là một dạng đệ quy.
Một tình huống thú vị khác gây ra lỗi này là nếu một lớp đang được khởi tạo trong cùng một lớp với một biến thể hiện của lớp đó . Điều này sẽ làm cho hàm tạo của cùng một lớp được gọi đi gọi lại (đệ quy) và cuối cùng dẫn đến lỗi StackOverflowError.
Trong phần tiếp theo, chúng ta sẽ xem xét một số ví dụ chứng minh những tình huống này.
3. StackOverflowError đang hoạt động
Trong ví dụ được hiển thị bên dưới, một StackOverflowError sẽ được ném ra do đệ quy ngoài ý muốn, trong đó nhà phát triển đã quên chỉ định điều kiện kết thúc cho hành vi đệ quy:
(adsbygoogle = window.adsbygoogle || []).push({});
public class UnintendedInfiniteRecursion { public int calculateFactorial(int number) { return number * calculateFactorial(number - 1); } }
Ở đây, lỗi được ném vào tất cả các trường hợp cho bất kỳ giá trị nào được truyền vào phương thức:
public class UnintendedInfiniteRecursionManualTest { @Test(expected = StackOverflowError.class) public void givenPositiveIntNoOne_whenCalFact_thenThrowsException() { int numToCalcFactorial= 1; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } @Test(expected = StackOverflowError.class) public void givenPositiveIntGtOne_whenCalcFact_thenThrowsException() { int numToCalcFactorial= 2; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } @Test(expected = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException() { int numToCalcFactorial= -1; UnintendedInfiniteRecursion uir = new UnintendedInfiniteRecursion(); uir.calculateFactorial(numToCalcFactorial); } }
Tuy nhiên, trong ví dụ tiếp theo, một điều kiện kết thúc được chỉ định nhưng sẽ không bao giờ được đáp ứng nếu giá trị -1 được chuyển đến phương thức calculateFactorial() , điều này gây ra đệ quy không kết thúc / vô hạn:
public class InfiniteRecursionWithTerminationCondition { public int calculateFactorial(int number) { return number == 1 ? 1 : number * calculateFactorial(number - 1); } }
Tổng hợp các bài kiểm tra này thể hiện tình huống này:
public class InfiniteRecursionWithTerminationConditionManualTest { @Test public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); assertEquals(1, irtc.calculateFactorial(numToCalcFactorial)); } @Test public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); assertEquals(120, irtc.calculateFactorial(numToCalcFactorial)); } @Test(expected = StackOverflowError.class) public void givenNegativeInt_whenCalcFact_thenThrowsException() { int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition(); irtc.calculateFactorial(numToCalcFactorial); } }
Trong trường hợp cụ thể này, lỗi hoàn toàn có thể tránh được nếu điều kiện chấm dứt đơn giản là:
public class RecursionWithCorrectTerminationCondition { public int calculateFactorial(int number) { return number <= 1 ? 1 : number * calculateFactorial(number - 1); } }
Đây là bài kiểm tra cho thấy tình huống này trong thực tế:
public class RecursionWithCorrectTerminationConditionManualTest { @Test public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = new RecursionWithCorrectTerminationCondition(); assertEquals(1, rctc.calculateFactorial(numToCalcFactorial)); } }
Bây giờ chúng ta hãy xem xét một kịch bản mà StackOverflowError xảy ra do mối quan hệ tuần hoàn giữa các lớp. Hãy xem xét ClassOne và ClassTwo , chúng khởi tạo lẫn nhau bên trong các hàm tạo của chúng gây ra mối quan hệ tuần hoàn:
(adsbygoogle = window.adsbygoogle || []).push({});
public class ClassOne { private int oneValue; private ClassTwo clsTwoInstance = null; public ClassOne() { oneValue = 0; clsTwoInstance = new ClassTwo(); } public ClassOne(int oneValue, ClassTwo clsTwoInstance) { this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; } }
public class ClassTwo { private int twoValue; private ClassOne clsOneInstance = null; public ClassTwo() { twoValue = 10; clsOneInstance = new ClassOne(); } public ClassTwo(int twoValue, ClassOne clsOneInstance) { this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; } }
Bây giờ, khởi tạo ClassOne như đã thấy trong bài kiểm tra này:
public class CyclicDependancyManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingClassOne_thenThrowsException() { ClassOne obj = new ClassOne(); } }
Điều này kết thúc bằng lỗi StackOverflowError vì phương thức khởi tạo của ClassOne đang khởi tạo ClassTwo và phương thức khởi tạo của ClassTwo lại là khởi tạo ClassOne. Và điều này liên tục xảy ra cho đến khi nó tràn ngăn xếp.
Tiếp theo, chúng ta sẽ xem xét điều gì sẽ xảy ra khi một lớp đang được khởi tạo trong cùng một lớp với một biến thể hiện của lớp đó.
Như đã thấy trong ví dụ tiếp theo, AccountHolder tự khởi tạo chính nó như một biến thể hiện JointAccountHolder:
public class AccountHolder { private String firstName; private String lastName; AccountHolder jointAccountHolder = new AccountHolder(); }
Khi lớp AccountHolder được khởi tạo , một lỗi StackOverflowError được đưa ra do lệnh gọi đệ quy của hàm tạo như được thấy trong thử nghiệm này:
public class AccountHolderManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException() { AccountHolder holder = new AccountHolder(); } }
4. Xử lý StackOverflowError
Điều tốt nhất nên làm khi gặp phải lỗi StackOverflowError là kiểm tra dấu vết ngăn xếp một cách thận trọng để xác định dạng lặp lại của số dòng. Điều này sẽ cho phép chúng ta xác định mã có vấn đề đệ quy.
Hãy xem xét một vài dấu vết ngăn xếp do các ví dụ mã mà chúng ta đã thấy trước đó.
(adsbygoogle = window.adsbygoogle || []).push({});
Dấu vết ngăn xếp này được tạo ra bởi InfiniteRecursionWithTerminationConditionManualTest nếu chúng ta bỏ qua phần khai báo ngoại lệ dự kiến:
java.lang.StackOverflowError
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
at c.b.s.InfiniteRecursionWithTerminationCondition
.calculateFactorial(InfiniteRecursionWithTerminationCondition.java:5)
Ở đây, dòng số 5 có thể được nhìn thấy đang lặp lại. Đây là nơi mà cuộc gọi đệ quy đang được thực hiện. Bây giờ nó chỉ là vấn đề kiểm tra mã để xem liệu đệ quy có được thực hiện đúng cách hay không.
Đây là dấu vết ngăn xếp mà chúng ta nhận được bằng cách thực thi CyclicDependancyManualTest (một lần nữa, không có ngoại lệ mong đợi):
java.lang.StackOverflowError
at c.b.s.ClassTwo.(ClassTwo.java:9)
at c.b.s.ClassOne.(ClassOne.java:9)
at c.b.s.ClassTwo.(ClassTwo.java:9)
at c.b.s.ClassOne.(ClassOne.java:9)
Dấu vết ngăn xếp này hiển thị số dòng gây ra sự cố trong hai lớp có mối quan hệ tuần hoàn. Dòng số 9 của ClassTwo và dòng số 9 của ClassOne trỏ đến vị trí bên trong phương thức khởi tạo nơi nó cố gắng khởi tạo lớp khác.
Khi mã đang được kiểm tra kỹ lưỡng và nếu không có điều nào sau đây (hoặc bất kỳ lỗi logic mã nào khác) là nguyên nhân gây ra lỗi:
- Đệ quy được triển khai không chính xác (tức là không có điều kiện kết thúc)
- Sự phụ thuộc tuần hoàn giữa các lớp
- Khởi tạo một lớp trong cùng một lớp với một biến thể hiện của lớp đó
Sẽ là một ý kiến hay nếu bạn thử tăng kích thước ngăn xếp. Tùy thuộc vào JVM được cài đặt, kích thước ngăn xếp mặc định có thể khác nhau.
-Xss flag có thể được sử dụng để tăng kích thước của ngăn xếp, từ cấu hình của dự án hoặc dòng lệnh.
5. Kết luận
Trong bài viết này, chúng ta đã xem xét kỹ hơn về lỗi StackOverflowError bao gồm cách mã Java có thể gây ra lỗi và cách có thể chẩn đoán và sửa lỗi.