StackOverflowError в Java

1. Общ преглед

StackOverflowError може да досажда на разработчиците на Java, тъй като това е една от най-често срещаните грешки по време на изпълнение, с които можем да се сблъскаме.

В тази статия ще видим как може да възникне тази грешка, като разгледаме различни примери за код, както и как можем да се справим с нея.

2. Рамки на стека и как възниква StackOverflowError

Нека започнем с основите. Когато се извика метод, в стека на повикванията се създава нов кадър на стека. Този кадър на стека съдържа параметри на извикания метод, неговите локални променливи и връщащия адрес на метода, т.е. точката, от която изпълнението на метода трябва да продължи след връщането на извикания метод.

Създаването на стекови рамки ще продължи, докато достигне края на извикванията на методите, намерени вътре в вложени методи.

По време на този процес, ако JVM срещне ситуация, при която няма място за създаване на нов кадър на стека, той ще хвърли StackOverflowError .

Най-честата причина за JVM да се сблъска с тази ситуация е неокончателна / безкрайна рекурсия - описанието на Javadoc за StackOverflowError споменава, че грешката е хвърлена в резултат на твърде дълбока рекурсия в определен кодов фрагмент.

Рекурсията обаче не е единствената причина за тази грешка. Това може да се случи и в ситуация, когато приложението продължава да извиква методи отвътре, докато стекът бъде изчерпан . Това е рядък случай, тъй като никой разработчик не би умишлено следвал лоши практики за кодиране. Друга рядка причина е наличието на огромен брой локални променливи вътре в метода .

В StackOverflowError също могат да бъдат изхвърлени, когато приложение е проектиран да има в цикличен взаимоотношения между класовете . В тази ситуация конструкторите един на друг се извикват многократно, което кара тази грешка да бъде хвърлена. Това може да се разглежда и като форма на рекурсия.

Друг интересен сценарий, който причинява тази грешка, е ако даден клас се екземпляри в същия клас като променлива на екземпляр на този клас . Това ще накара конструктора от същия клас да бъде извикан отново и отново (рекурсивно), което в крайна сметка води до StackOverflowError.

В следващия раздел ще разгледаме някои примери за код, които демонстрират тези сценарии.

3. StackOverflowError в действие

В примера, показан по-долу, StackOverflowError ще бъде хвърлен поради неволна рекурсия, където разработчикът е забравил да посочи условие за прекратяване на рекурсивното поведение:

public class UnintendedInfiniteRecursion { public int calculateFactorial(int number) { return number * calculateFactorial(number - 1); } }

Тук грешката се хвърля при всички случаи за всяка стойност, предадена в метода:

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); } }

В следващия пример обаче е посочено условие за прекратяване, но никога не е изпълнено, ако на метода CalcuFactorial () се предаде стойност -1 , което причинява неокончателна / безкрайна рекурсия:

public class InfiniteRecursionWithTerminationCondition { public int calculateFactorial(int number) { return number == 1 ? 1 : number * calculateFactorial(number - 1); } }

Този набор от тестове демонстрира този сценарий:

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); } }

В този конкретен случай грешката можеше да бъде напълно избегната, ако условието за прекратяване беше просто поставено като:

public class RecursionWithCorrectTerminationCondition { public int calculateFactorial(int number) { return number <= 1 ? 1 : number * calculateFactorial(number - 1); } }

Ето теста, който показва този сценарий на практика:

public class RecursionWithCorrectTerminationConditionManualTest { @Test public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc() { int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = new RecursionWithCorrectTerminationCondition(); assertEquals(1, rctc.calculateFactorial(numToCalcFactorial)); } }

Сега нека разгледаме сценарий, при който StackOverflowError се случва в резултат на циклични връзки между класове. Нека разгледаме ClassOne и ClassTwo , които се инстанцират взаимно в своите конструктори, причинявайки циклична връзка:

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; } }

Сега да кажем, че се опитваме да създадем екземпляр на ClassOne, както се вижда в този тест:

public class CyclicDependancyManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingClassOne_thenThrowsException() { ClassOne obj = new ClassOne(); } }

Това завършва със StackOverflowError, тъй като конструкторът на ClassOne създава инстанция ClassTwo, а конструкторът на ClassTwo отново създава инстанция ClassOne. И това се случва многократно, докато не препълни стека.

След това ще разгледаме какво се случва, когато даден клас се създава в рамките на същия клас като променлива на екземпляр на този клас.

Както се вижда в следващия пример, AccountHolder се инстанцира като променлива на екземпляр jointAccountHolder :

public class AccountHolder { private String firstName; private String lastName; AccountHolder jointAccountHolder = new AccountHolder(); }

When the AccountHolder class is instantiated, a StackOverflowError is thrown due to the recursive calling of the constructor as seen in this test:

public class AccountHolderManualTest { @Test(expected = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException() { AccountHolder holder = new AccountHolder(); } }

4. Dealing With StackOverflowError

The best thing to do when a StackOverflowError is encountered is to inspect the stack trace cautiously to identify the repeating pattern of line numbers. This will enable us to locate the code that has problematic recursion.

Let's examine a few stack traces caused by the code examples we saw earlier.

This stack trace is produced by InfiniteRecursionWithTerminationConditionManualTest if we omit the expected exception declaration:

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)

Here, line number 5 can be seen repeating. This is where the recursive call is being done. Now it's just a matter of examining the code to see if the recursion is done in a correct manner.

Here is the stack trace we get by executing CyclicDependancyManualTest (again, without expected exception):

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)

This stack trace shows the line numbers that cause the problem in the two classes that are in a cyclic relationship. Line number 9 of ClassTwo and line number 9 of the ClassOne point to the location inside the constructor where it tries to instantiate the other class.

Once the code is being thoroughly inspected and if none of the following (or any other code logic error) is the cause of the error:

  • Неправилно внедрена рекурсия (т.е. без условие за прекратяване)
  • Циклична зависимост между класовете
  • Инстанциране на клас в същия клас като променлива на екземпляр на този клас

Би било добра идея да опитате да увеличите размера на стека. В зависимост от инсталираната JVM, размерът на стека по подразбиране може да варира.

В -Xss знамето могат да бъдат използвани за увеличаване на размера на пакета, или от конфигурацията на проекта или командния ред.

5. Заключение

В тази статия разгледахме по-отблизо StackOverflowError, включително как Java кодът може да го причини и как можем да го диагностицираме и поправим.

Изходният код, свързан с тази статия, може да бъде намерен в GitHub.