Помилка Stack overflow - що це і як її уникнути?
зазвичай це означає, що є проблеми з рекурсією, в більшості випадків коли задається це питання - немає умови виходу з рекурсії.
Ще з'являється, коли ви оголошує занадто великий локальний масив (всередині функції). Зазвичай розмір стека обмежений кількома мегабайтами.
2 відповіді 2
Це означає, що у стеку недостатньо місця.
Причини - наприклад, занадто глибока рекурсія (рідко), або занадто великі локальні змінні (куди частіше), або те й інше відразу :)
Як позбутися? Знову ж таки, можна просто в налаштуваннях компілятора підняти розмір стека.
Але надійніше і краще - подивитися, чи немає занадто глибокої (аж до нескінченності) рекурсії, замінити локальні масиви на динамічно, що виділяються.
практично гарантовано дасть переповнення стеку. На відміну від
Словом, дивіться, хто з'їдає багато стекової пам'яті, і позбавляйтеся його.
Про стек та інші типи даних:
- Стічні дані. Там пам'ять виділяється під час заходу у процедуру і звільняється за її завершенні. Максимальний розмір стека програми складає 1 GB і для 32-бітових, і для 64-бітових програм. (Розмірстека задається лінковником і за замовчуванням становить 1 MB)
- Статичні дані. Обмеження накладається на розмір самого вихідного коду програми і розмір пам'яті, що статично виділяється. У мові C++ такі дані зазвичай представлені змінними, оголошеними глобальному рівні поза процедурами. Як для 32-бітних, так і для 64-бітових програм, обмеження на розмір пам'яті, що статично виділяється, дорівнює 2 GB.
- Динамічні дані Це дані, пам'ять на які динамічно виділяється під час виконання програми. У C++ таке виділення зазвичай здійснюється функцією malloc чи оператором new. У 32-бітових програмах розмір пам'яті, що динамічно виділяється, обмежений 2 GB, в 64-бітних - 8 TB.
У 32-бітної програми, запущеної в 32-бітній Windows, сумарний розмір всіх перелічених типів даних не повинен перевищувати 2 GB. (Практично обмеження дорівнює 1.75GB через вимоги до пам'яті операційної системи) 32-бітна програма, зібрана з ключем /LARGEADDRESSAWARE:YES може виділяти до 3-х гігабайт пам'яті, якщо 32-бітна операційна система Windows запущена з ключем /3gb.Ця ж 32-бітна програма, запущена на 64-бітовій системі, може виділити майже 4 GB пам'яті (на практиці близько 3.5 GB).
Обмеження на максимальний розмір статично-виділеної та стекової пам'яті однакові для 32-х та 64-х бітних Windows додатків. Це пов'язано з форматом типу файлів Portable Executable (PE), який використовується у Windows для опису exe та dll файлів. Статичні та стікові дані розташовуються в перших 2-х GB адресного простору програми. Варто пам'ятати, що дані обмеження накладаються операційною системою і не залежать від використовуваного компілятора.
Це означає, що у стеку недостатньо місця.
- Знаючи вище описані обмеження та можливі причини появи цієї помилки, Ви завжди можете проектувати Ваші рішення, щоб уникати подібних проблем.
Що насправді викликає помилку StackOverflow у Java?
StackOverflowError просто сигналізує про те, що пам'яті більше немає. Він розширює клас VirtualMachineError, що вказує на те, що JVM (віртуальна машина Java) пошкоджена або у неї закінчилися ресурси і вона не може працювати.
Якщо у вас є така функція, як:
У наведеному вище коді myFunction() буде продовжувати називати себе все глибше і глибше, і коли простір, що використовується для відстеження того, які функції ви знаходитесь, заповнено, ви отримуєте помилку stackoverflow. Загальною причиною переповнення стека є поганий рекурсивний виклик. Як правило, це викликано, коли ваші рекурсивні функції не мають правильної умови завершення, тому він закінчується тим, що назавжди називає себе.
Причина використання StackOverflowError
Параметри та локальні змінні виділяються у стеку. Стек зазвичай знаходиться у верхньому кінці адресного простору, і, оскільки він використовується, він прямує до нижньої частини адресного простору. Ваш процес також має купу, яка живе в нижній частині вашого процесу. Коли ви виділяєте пам'ять, ця купа може зростати у верхньому кінці вашого адресного простору. Як ви можете бачити, існує ймовірність того, що купа зіткнеться зі склом. Якщо немає місця для нового стека кадрів, StackOverflowError викликається віртуальною машиною Java (JVM).
Якщо стек заповнений, ви не можете натиснути, якщо ви зробите це, ви отримаєте помилку переповнення стека.
Якщо стек порожній, ви не можете поп, якщо ви це зробите, ви отримаєте помилку стек стека.
Що таке stacktrace?
Стовпець – дуже корисний інструмент для налагодження. Це список викликів методу, в яких програма була посередині, коли було вибрано виняток. Це дуже корисно, тому що воно не тільки показує вам, де сталася помилка, але також і те, як програма опинилася в цьому місці коду.
Стовпець – дуже корисний інструмент для налагодження. Це список викликів методу, в яких програма була посередині, коли було вибрано виняток. Це дуже корисно, тому що воно не тільки показує вам, де сталася помилка, але також і те, як програма опинилася в цьому місці коду.
StackOverflowError в Java
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);
>
>
Однак у наступному прикладі вказано умову завершення, але вона ніколи не виконується, якщо методу calculateFactorial() передається значення -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
колиInstanciatingClassOne_thenThrowsException()
ClassOne
obj
=
new
ClassOne();
>
>
Це закінчується StackOverflowError, оскільки конструктор ClassOne створює екземпляр ClassTwo, а конструктор ClassTwo знову створює екземпляр ClassOne. І це повторюється неодноразово, доки не переповниться стек.
Далі ми розглянемо, що відбувається, коли екземпляр класу створюється у тому класі, як і змінна екземпляра цього.
Як видно з наступного прикладу, AccountHolder інстанцує себе як змінну екземпляруjoinAccountHolder :
public
class
AccountHolder
private
String
firstName;
private
String
lastName;
AccountHolder
jointAccountHolder
=
new
AccountHolder();
>
Коли створюється екземпляр класу AccountHolder , видається StackOverflowError через рекурсивний виклик конструктора, як показано в цьому тесті:
public
class
AccountHolderManualTest
@Test(expected
=
StackOverflowError.class)
public
void
колиInstanciatingAccountHolder_thenThrowsException()
AccountHolder
holder
=
new
AccountHolder();
>
>
4. Робота з StackOverflowError
При виявленні помилки StackOverflowError найкраще уважно вивчити трасування стека, щоб визначити шаблон номерів рядків, що повторюється. Це дозволить нам знайти код із проблемною рекурсією.
Давайте розглянемо кілька трасування стека, викликаних прикладами коду, які ми бачили раніше.
Це трасування стека створюється InfiniteRecursionWithTerminationConditionManualTest , якщо ми опускаємо очікуване оголошення виключення:
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 (знову ж таки, без очікуваного виключення):
java.lang.StackOverflowError
at
c.b.s.ClassTwo.init>(ClassTwo.java:9)
at
c.b.s.ClassOne.init>(ClassOne.java:9)
at
c.b.s.ClassTwo.init>(ClassTwo.java:9)
at
c.b.s.ClassOne.init>(ClassOne.java:9)
Це трасування стека показує номери рядків, які викликають проблему в двох класах, що знаходяться в циклічному зв'язку. Рядок номер 9 ClassTwo та рядок номер 9 ClassOne вказують на місце всередині конструктора, де він намагається створити екземпляр іншого класу.
Після ретельної перевірки коду і якщо жодна з наступних (або будь-яка інша логічна помилка коду) не є причиною помилки:
- Неправильно реалізована рекурсія (тобто без умови завершення)
- Циклічна залежність між класами
- Створення екземпляра класу у тому класі, як і змінна екземпляра цього класу
Було б непогано спробувати збільшити розмір стека. Залежно від встановленої JVM, розмір стека за замовчуванням може відрізнятися.
Прапор -Xss можна використовувати для збільшення розміру стека або конфігурації проекту, або з командного рядка.
5. Висновок
У цій статті ми детальніше розглянули StackOverflowError , у тому числі те, як Java може викликати її, і як ми можемо її діагностувати і виправити.
Вихідний код, який стосується цієї статті, можна знайти на GitHub .
