- StackOverflowError в Java
- 2. Фреймы стека и как происходитStackOverflowError
- 3. StackOverflowError в действии
- 4. Работа сStackOverflowError
- 5. Заключение
- Java StackOverFlowError Causes & Solutions
- 1. StackOverFlowError
- 2. StackOverFlowError Cause
- 3. Solutions to StackOverflowError
- 3.1 Fix the Code
- 3.2 Increase Thread Stack Size (-Xss)
StackOverflowError в Java
StackOverflowError может раздражать разработчиков Java, поскольку это одна из самых распространенных ошибок времени выполнения, с которыми мы можем столкнуться.
В этой статье мы увидим, как может возникать эта ошибка, на различных примерах кода и узнаем, как с ней бороться.
2. Фреймы стека и как происходитStackOverflowError
Давайте начнем с основ. When a method is called, a new stack frame gets created on the call stack. Этот кадр стека содержит параметры вызванного метода, его локальные переменные и адрес возврата метода, т.е. точка, с которой выполнение метода должно продолжаться после возврата вызванного метода.
Создание кадров стека будет продолжаться до тех пор, пока не будет достигнут конец вызовов методов, найденных во вложенных методах.
Во время этого процесса, если JVM обнаруживает ситуацию, когда нет места для создания нового кадра стека, она выдастStackOverflowError.
Наиболее частая причина, по которой JVM может столкнуться с этой ситуацией, —unterminated/infinite recursion — в описании Javadoc дляStackOverflowError упоминается, что ошибка возникает в результате слишком глубокой рекурсии в конкретном фрагменте кода.
Однако рекурсия не является единственной причиной этой ошибки. Это также может произойти в ситуации, когда приложение хранитcalling methods from within methods until the stack is exhausted. Это редкий случай, так как ни один разработчик не будет намеренно следовать плохой практике кодирования Другая редкая причина —having a vast number of local variables inside a method.
StackOverflowError также может быть выброшено, если приложение разработано для использованияcyclic relationships between classes. В этой ситуации конструкторы друг друга вызывают неоднократно, что приводит к возникновению этой ошибки. Это также можно рассматривать как форму рекурсии.
Другой интересный сценарий, который вызывает эту ошибку, — этоclass is being instantiated within the same class as an instance variable of that class. Это приведет к тому, что конструктор одного и того же класса будет вызываться снова и снова (рекурсивно), что в конечном итоге приведет к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); >>
Однако в следующем примере указано условие завершения, но оно никогда не выполняется, если значение-1 передается методуcalculateFactorial(), что вызывает незавершенную / бесконечную рекурсию:
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 >
Вот тест, который демонстрирует этот сценарий на практике:
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
Когда создается экземпляр классаAccountHolder,, вызываетсяStackOverflowError из-за рекурсивного вызова конструктора, как показано в этом тесте:
public class AccountHolderManualTest < @Test(expected = StackOverflowError.class) public void whenInstanciatingAccountHolder_thenThrowsException() < AccountHolder holder = new AccountHolder(); >>
4. Работа сStackOverflowError
Лучшее, что можно сделать при обнаруженииStackOverflowError, — это осторожно изучить трассировку стека, чтобы определить повторяющийся шаблон номеров строк. Это позволит нам найти код с проблемной рекурсией.
Давайте рассмотрим несколько трассировок стека, вызванных примерами кода, которые мы видели ранее.
Эта трассировка стека создаетсяInfiniteRecursionWithTerminationConditionManualTest, если мы опускаем объявление исключенияexpected:
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)
Здесь строка № 5 видна повторяющейся. Это где рекурсивный вызов делается. Теперь остается просто изучить код, чтобы убедиться, что рекурсия выполнена правильно.
Вот трассировка стека, которую мы получаем, выполняяCyclicDependancyManualTest (опять же, без исключенияexpected):
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)
Эта трассировка стека показывает номера строк, которые вызывают проблему в двух классах, которые находятся в циклическом отношении. Строка номер 9ClassTwo и строка номер 9ClassOne указывают на место внутри конструктора, где он пытается создать экземпляр другого класса.
Как только код тщательно проверен, и если ни одно из следующего (или любая другая логическая ошибка кода) не является причиной ошибки:
- Неправильно реализованная рекурсия (т.е. без условия прекращения)
- Циклическая зависимость между классами
- Создание класса внутри того же класса, что и переменная экземпляра этого класса
Было бы неплохо попробовать увеличить размер стека. В зависимости от установленной JVM размер стека по умолчанию может варьироваться.
Флаг-Xss можно использовать для увеличения размера стека либо из конфигурации проекта, либо из командной строки.
5. Заключение
В этой статье мы более подробно рассмотрелиStackOverflowError, включая то, как код Java может вызывать это, и как мы можем диагностировать и исправить это.
Исходный код, относящийся к этой статье, можно найтиover on GitHub.
Java StackOverFlowError Causes & Solutions
StackOverFlowError is one of the common JVM error. In this blog post, let’s learn inner mechanics of thread stacks, reasons that can trigger StackOverFlowError and potential solutions to address this error.
1. StackOverFlowError
To gain a deeper understanding of this exception, let’s review this simple program:
public class SimpleExample < public static void main(String args[]) < a(); >public static void a() < int x = 0; b(); >public static void b() < Car y = new Car(); c(); >public static void c() < float z = 0 f; System.out.println("Hello"); >>
This program is very simple with the following execution code:
- main() method invoked first
- main() method invokes a() method. Inside a() method integer variable ‘x’ is initialized to value 0.
- a() method in turn invokes b() method. Inside b() method Car object constructed and assigned to variable ‘y’.
- b() method in turn invokes c() method. Inside c() method float variable ‘z’ is initialized to value 0.
Now let’s review what happens behind the scenes when the above simple program executed. Each thread in the application has its own stack. Each stack has multiple stack frames. Thread adds the methods it’s executing, primitive data types, object pointers, return values to its stack frame in the sequence order in which they got executed.
In step #1: main() method pushed into the application thread’s stack. Step #2: a() method pushed into application thread’s stack. In a() method, primitive data type ‘int’ is defined with value 0 and assigned to variable x. This information also pushed into the same stack frame. Note both data i.e. ‘0’ and variable ‘x’ pushed into thread’s stack frame.
In step #3: b() method pushed into thread’s stack. In b() method, ‘Car’ object created and assigned to variable ‘y’. A crucial point to note here is ‘Car’ object created in the heap and not in the thread’s stack. Only Car object’s reference i.e. y stored in the thread’s stack frame.
In step #4: c() method pushed into thread’s stack. In c() method, primitive data type ‘float’ is defined with value 0f and assigned to variable z. This information is also pushed into the same stack frame. Note both data i.e. ‘0f’ and variable ‘z’ is pushed into thread’s stack frame.
2. StackOverFlowError Cause
As you can see thread’s stack is storing methods it’s executing, primitive data types, variables, object pointers and return values. All of these consume memory. If thread’s stack sizes grow beyond the allocated memory limit then StackOverflowError is thrown. Let’s look at the below buggy program, which will result in StackOverflowError:
public class SOFDemo < public static void a() < // Buggy line. It will cause method a() to be called infinite number of times. a(); >public static void main(String args[]) < a(); >>
In this program main() method invokes a() method. a() method recursively calls itself. This implementation will cause a() method to be invoked an infinite number of times. In this circumstance, a() method got added to thread’s stack frame an infinite number of times. Thus, after a few thousand iterations thread’s stack size limit might exceeded. Once stack size limit exceeds, it will result in ‘StackOverflowError’:
Exception in thread "main" java.lang.StackOverflowError at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7) at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7) at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7) at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7) at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7) at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7) at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7) at com.buggyapp.stackoverflow.SOFDemo.a(SOFDemo.java:7)
3. Solutions to StackOverflowError
There are a couple of strategies to address StackOverflowError.
3.1 Fix the Code
Because of a non-terminating recursive call (as shown in the above example), threads stack size can grow to a large size. In that circumstance, you must fix the source code which is causing recursive looping. When ‘StackOverflowError’ is thrown, it will print the stacktrace of the code that it was recursively executing. This code is a good pointer to start debugging and fixing the issue. In the above example it’s ‘ a() ’ method.
3.2 Increase Thread Stack Size (-Xss)
There might be a legitimate reason where a threads stack size needs an increment. Maybe thread has to execute a large number of methods or lot of local variables/created in the methods thread has been executing. In such circumstance, you can increase the thread’s stack size using the JVM argument: ‘-Xss’. Pass this argument when you start the application. Example:
This will set the thread’s stack size to 2 Mb. It might bring a question, what is the default thread’s stack size? Default thread stack size varies based on your operating system, java version & vendor.
JVM version
Thread stack size