Что такое StackruError?
Что такое StackruError
чем это вызвано и как с ними бороться?
17 ответов
Параметры и локальные переменные размещаются в стеке (со ссылочными типами объект живет в куче, а переменная ссылается на этот объект). Стек обычно располагается в верхнем конце вашего адресного пространства и по мере его использования он направляется к нижней части адресного пространства (т. Е. К нулю).
У вашего процесса также есть куча, которая живет в нижней части вашего процесса. По мере выделения памяти эта куча может увеличиваться в направлении верхнего края вашего адресного пространства. Как вы видите, куча может "столкнуться" со стеком (немного похоже на тектонические плиты!!!).
Распространенной причиной переполнения стека является неправильный рекурсивный вызов. Как правило, это происходит, когда ваши рекурсивные функции не имеют правильного условия завершения, поэтому они в конечном итоге вызывают себя навсегда.
Однако с программированием GUI возможно генерировать косвенную рекурсию. Например, ваше приложение может обрабатывать сообщения рисования и во время их обработки может вызывать функцию, которая заставляет систему отправлять другое сообщение рисования. Здесь вы не назвали себя явно, но OS/VM сделала это за вас.
Чтобы разобраться с ними, вам нужно изучить свой код. Если у вас есть функции, которые вызывают сами себя, проверьте, что у вас есть условие завершения. Если да, то убедитесь, что при вызове функции вы как минимум изменили один из аргументов, иначе не будет видимых изменений для рекурсивно вызываемой функции и условие завершения бесполезно.
Если у вас нет очевидных рекурсивных функций, проверьте, вызываете ли вы какие-либо библиотечные функции, которые косвенно вызовут вашу функцию (как неявный случай выше).
Чтобы описать это, сначала давайте разберемся, как хранятся локальные переменные и объекты.
Локальные переменные хранятся в стеке:
Если вы посмотрели на изображение, вы должны понимать, как все работает.
Когда вызов функции вызывается приложением Java, в стеке вызовов выделяется кадр стека. Кадр стека содержит параметры вызванного метода, его локальные параметры и адрес возврата метода. Адрес возврата обозначает точку выполнения, с которой выполнение программы должно продолжаться после возврата вызванного метода. Если для нового кадра стека нет места, StackruError
выбрасывается виртуальной машиной Java (JVM).
Наиболее распространенным случаем, который может исчерпать стек Java-приложения, является рекурсия. В рекурсии метод вызывает себя во время выполнения. Рекурсия считается мощной техникой программирования общего назначения, но ее следует использовать с осторожностью, чтобы избежать StackruError
,
Пример бросания StackruError
показано ниже:
StackruErrorExample.java:
public class StackruErrorExample {
public static void recursivePrint(int num) {
System.out.println("Number: " + num);
if(num == 0)
return;
else
recursivePrint(++num);
}
public static void main(String[] args) {
StackruErrorExample.recursivePrint(1);
}
}
В этом примере мы определяем рекурсивный метод, называемый recursivePrint
он печатает целое число, а затем вызывает сам себя, используя следующее последовательное целое число в качестве аргумента. Рекурсия заканчивается, пока мы не пройдем 0
в качестве параметра. Однако в нашем примере мы передали параметр от 1 и его растущих последователей, следовательно, рекурсия никогда не завершится.
Пример выполнения с использованием -Xss1M
Флаг, который задает размер стека потока равным 1 МБ, показан ниже:
Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" java.lang.StackruError
at java.io.PrintStream.write(PrintStream.java:480)
at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.write(PrintStream.java:527)
at java.io.PrintStream.print(PrintStream.java:669)
at java.io.PrintStream.println(PrintStream.java:806)
at StackruErrorExample.recursivePrint(StackruErrorExample.java:4)
at StackruErrorExample.recursivePrint(StackruErrorExample.java:9)
at StackruErrorExample.recursivePrint(StackruErrorExample.java:9)
at StackruErrorExample.recursivePrint(StackruErrorExample.java:9)
...
В зависимости от начальной конфигурации JVM результаты могут отличаться, но в конечном итоге StackruError
должен быть брошен. Этот пример является очень хорошим примером того, как рекурсия может вызвать проблемы, если она не реализована с осторожностью.
Как бороться с StackruError
Самое простое решение - тщательно осмотреть трассировку стека и обнаружить повторяющуюся последовательность номеров строк. Эти номера строк указывают на рекурсивно вызываемый код. Как только вы обнаружите эти строки, вы должны тщательно проверить свой код и понять, почему рекурсия никогда не заканчивается.
Если вы убедились, что рекурсия реализована правильно, вы можете увеличить размер стека, чтобы разрешить большее количество вызовов. В зависимости от установленной виртуальной машины Java (JVM) размер стека потока по умолчанию может быть равен 512 КБ или 1 МБ. Вы можете увеличить размер стека потока, используя
-Xss
флаг. Этот флаг можно указать либо через конфигурацию проекта, либо через командную строку. Формат-Xss
Аргумент:-Xss<size>[g|G|m|M|k|K]
Если у вас есть такая функция:
int foo()
{
// more stuff
foo();
}
Тогда foo() будет продолжать вызывать себя, становясь все глубже и глубже, и когда пространство, используемое для отслеживания того, в каких функциях вы находитесь, заполнено, вы получите ошибку переполнения стека.
Переполнение стека означает именно это: переполнение стека. Обычно в программе есть один стек, который содержит переменные локальной области и адреса, куда возвращаться, когда выполнение подпрограммы заканчивается. Этот стек имеет тенденцию быть фиксированным диапазоном памяти где-то в памяти, поэтому он ограничен, насколько он может содержать значения.
Если стек пуст, вы не можете всплыть, если вы это сделаете, вы получите ошибку переполнения стека.
Если стек заполнен, вы не можете нажать, если вы это сделаете, вы получите ошибку переполнения стека.
Таким образом, переполнение стека появляется, когда вы размещаете слишком много в стеке. Например, в упомянутой рекурсии.
Некоторые реализации оптимизируют некоторые формы рекурсий. Хвостовая рекурсия в частности. Хвостовые рекурсивные подпрограммы являются формой подпрограмм, в которых рекурсивный вызов представляется как последнее, что делает подпрограмма. Такой обычный вызов просто сводится к прыжку.
Некоторые реализации зашли так далеко, что реализуют свои собственные стеки для рекурсии, поэтому они позволяют рекурсии продолжаться до тех пор, пока в системе не будет исчерпано памяти.
Самое простое, что вы можете попробовать - это увеличить размер стека, если можете. Однако если вы не можете этого сделать, то лучше всего посмотреть, есть ли что-то, что явно вызывает переполнение стека. Попробуйте, напечатав что-нибудь до и после звонка в рутину. Это поможет вам найти рутину.
Переполнение стека обычно вызывается слишком глубокими вызовами вложенных функций (особенно легко при использовании рекурсии, то есть функции, которая вызывает себя) или выделении большого объема памяти в стеке, где использование кучи было бы более уместным.
Как вы говорите, вам нужно показать некоторый код.:-)
Ошибка переполнения стека обычно возникает, когда ваша функция вызывает гнездо слишком глубоко. Посмотрите ветку Stack Overflow Code Golf для некоторых примеров того, как это происходит (хотя в случае этого вопроса ответы намеренно вызывают переполнение стека).
StackruError
это ошибка времени выполнения в Java.
Он генерируется, когда объем памяти стека вызовов, выделенной JVM, превышен.
Распространенный случай аа StackruError
бросается, когда стек вызовов превышает из-за чрезмерной глубокой или бесконечной рекурсии.
Пример:
public class Factorial {
public static int factorial(int n){
if(n == 1){
return 1;
}
else{
return n * factorial(n-1);
}
}
public static void main(String[] args){
System.out.println("Main method started");
int result = Factorial.factorial(-1);
System.out.println("Factorial ==>"+result);
System.out.println("Main method ended");
}
}
Трассировки стека:
Main method started
Exception in thread "main" java.lang.StackruError
at com.program.stackru.Factorial.factorial(Factorial.java:9)
at com.program.stackru.Factorial.factorial(Factorial.java:9)
at com.program.stackru.Factorial.factorial(Factorial.java:9)
В приведенном выше случае можно избежать внесения программных изменений. Но если логика программы правильная и все-таки происходит, размер стека необходимо увеличить.
StackruError
в стек как OutOfMemoryError
это в кучу.
Неограниченные рекурсивные вызовы приводят к использованию стекового пространства.
Следующий пример производит StackruError
:
class StackruDemo
{
public static void unboundedRecursiveCall() {
unboundedRecursiveCall();
}
public static void main(String[] args)
{
unboundedRecursiveCall();
}
}
StackruError
можно избежать, если рекурсивные вызовы ограничены, чтобы общая сумма неполных вызовов в памяти (в байтах) не превышала размер стека (в байтах).
Вот пример рекурсивного алгоритма обращения к односвязному списку. На ноутбуке со следующей спецификацией (память 4G, процессор Intel Core i5 2,3 ГГц, 64-разрядная ОС Windows 7) эта функция приведет к ошибке Stackru для связанного списка размером, близким к 10000.
Я хочу сказать, что мы должны использовать рекурсию разумно, всегда принимая во внимание масштаб системы. Часто рекурсию можно преобразовать в итеративную программу, которая лучше масштабируется. (Одна итерационная версия того же алгоритма приведена в нижней части страницы, она переворачивает односвязный список размером 1 миллион за 9 миллисекунд.)
private static LinkedListNode doReverseRecursively(LinkedListNode x, LinkedListNode first){
LinkedListNode second = first.next;
first.next = x;
if(second != null){
return doReverseRecursively(first, second);
}else{
return first;
}
}
public static LinkedListNode reverseRecursively(LinkedListNode head){
return doReverseRecursively(null, head);
}
Итерационная версия того же алгоритма:
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
private static LinkedListNode doReverseIteratively(LinkedListNode x, LinkedListNode first) {
while (first != null) {
LinkedListNode second = first.next;
first.next = x;
x = first;
if (second == null) {
break;
} else {
first = second;
}
}
return first;
}
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
Наиболее распространенной причиной переполнения стека является чрезмерно глубокая или бесконечная рекурсия. Если это ваша проблема, это руководство по Java Recursion может помочь понять проблему.
У стека есть ограничение по объему, которое зависит от ОС, нормальный размер составляет 8 МБ (в Ubuntu вы можете проверить это ограничение с помощью $ ulimit -u
Аналогично можно проверить и в другой ОС). Любая программа использует стек во время выполнения, но чтобы полностью знать, когда он используется, вам нужно проверить язык ассемблера. Например, в x86_64 стек используется для:
- Сохранить адрес возврата при вызове процедуры
- Сохранить локальные переменные
- Сохраняйте специальные регистры, чтобы восстановить их позже
- Передача аргументов в вызов процедуры (более 6)
- Другое: случайная неиспользуемая база стека, канареечные значения, отступы и т. Д.
Если вы не знаете x86_64 (нормальный случай), вам нужно только знать, когда конкретный язык программирования высокого уровня, который вы используете, компилируется для этих действий. Например, в C:
- (1) - вызов функции
- (2) - локальные переменные в вызовах функций (включая основные)
- (3) â † 'локальные переменные в вызовах функций (не основные)
- (4) - вызов функции
- (5) - обычно это вызов функции, обычно это не имеет отношения к переполнению стека.
Итак, в C стек используют только локальные переменные и вызовы функций. Два (уникальных?) Способа переполнения стека:
- Объявление слишком больших локальных переменных в main или любой вызываемой им функции (
int array[10000][10000];
) - Очень глубокая или бесконечная рекурсия (слишком много вызовов функций одновременно).
Чтобы избежать StackruError
Вы можете:
проверьте, не слишком ли велики локальные переменные (порядка 1 МБ) - используйте кучу (вызовы malloc / calloc) или глобальные переменные.
проверьте бесконечную рекурсию - вы знаете, что делать... исправьте это!
проверьте нормальную и слишком глубокую рекурсию - самый простой подход - просто изменить реализацию на итеративную.
Также обратите внимание, что глобальные переменные, включаемые библиотеки и т. Д. Не используют стек.
Только если вышеуказанное не работает, измените размер стека на максимальный для конкретной ОС. Например, с Ubuntu:$ ulimit -s 32768
(32 МБ). (Это никогда не было решением ни одной из моих ошибок переполнения стека, но у меня также нет большого опыта)
Я пропустил специальные и / или нестандартные случаи в C (например, использование alloc()
и тому подобное), потому что если вы их используете, вы уже должны точно знать, что делаете.
Ура!
В случае кризиса, ситуация ниже приведет к ошибке переполнения стека.
public class Example3 {
public static void main(String[] args) {
main(new String[1]);
}
}
Простой пример Java, который вызывает ошибку java.lang.StackOverflowError из-за неправильного рекурсивного вызова
class Human {
Human(){
new Animal();
}
}
class Animal extends Human {
Animal(){
super();
}
}
public class Test01 {
public static void main(String[] args) {
new Animal();
}
}
Многие ответы на этот вопрос хороши. Однако я хотел бы применить несколько иной подход и дать некоторое представление о том, как работает память, а также (упрощенную) визуализацию, чтобы лучше понять ошибки StackOverflow. Это понимание применимо не только к Java, но и ко всем процессам.
В современных системах все новые процессы получают собственное виртуальное адресное пространство (VAS) . По сути, VAS — это уровень абстракции, предоставляемый операционной системой поверх физической памяти, чтобы гарантировать, что процессы не мешают памяти друг друга. Задача ядра состоит в том, чтобы затем сопоставить предоставленные виртуальные адреса с реальными физическими адресами.
VAS можно разделить на несколько разделов:
Чтобы ЦП знал, что он должен делать, машинные инструкции должны быть загружены в память. Это обычно называется сегментом кода или текста и имеет статический размер.
Кроме того, можно найти сегмент данных и кучу . Сегмент данных имеет фиксированный размер и содержит глобальные или статические переменные. Когда программа работает в особых условиях, ей может потребоваться дополнительное выделение данных, и именно здесь куча вступает в игру и, следовательно, может динамически увеличиваться в размере.
Стек расположен по другую сторону виртуального адресного пространства и (среди прочего) отслеживает все вызовы функций, используя структуру данных LIFO . Подобно куче, программе может потребоваться дополнительное пространство во время выполнения, чтобы отслеживать вызовы новых функций. Поскольку стек расположен на другой стороне VAS, он растет в противоположном направлении, то есть в сторону кучи.
TL;DR
Вот тут-то и возникает ошибка StackOverflow .
Поскольку стек растет вниз (по направлению к куче), может случиться так, что в какой-то момент времени он не сможет расти дальше, так как будет перекрываться с адресным пространством кучи. Как только это происходит, возникает ошибка StackOverflow.
Наиболее распространенная причина того, почему это происходит, связана с ошибкой в программе, делающей рекурсивные вызовы, которые не завершаются должным образом.
Обратите внимание, что на некоторых системах VAS может вести себя несколько иначе и может быть разделен на еще большее количество сегментов, однако это общее понимание применимо ко всем системам UNIX.
Термин "переполнение стека (переполнение)" часто используется, но неправильно; Атаки не переполняют стек, но буферизуют в стеке.
- из слайдов лекции профессора доктора Дитера Голлмана
Вот пример
public static void main(String[] args) {
System.out.println(add5(1));
}
public static int add5(int a) {
return add5(a) + 5;
}
Ошибка StackruError в основном возникает, когда вы пытаетесь сделать что-то, что, скорее всего, вызывает себя и продолжается бесконечно (или до тех пор, пока не выдаст ошибку StackruError).
add5(a)
позвоню сам, а потом снова позвоню и так далее.
Это типичный случай java.lang.StackruError
... Метод рекурсивно вызывает себя без выхода в doubleValue()
, floatValue()
, так далее.
Rational.java
public class Rational extends Number implements Comparable<Rational> {
private int num;
private int denom;
public Rational(int num, int denom) {
this.num = num;
this.denom = denom;
}
public int compareTo(Rational r) {
if ((num / denom) - (r.num / r.denom) > 0) {
return +1;
} else if ((num / denom) - (r.num / r.denom) < 0) {
return -1;
}
return 0;
}
public Rational add(Rational r) {
return new Rational(num + r.num, denom + r.denom);
}
public Rational sub(Rational r) {
return new Rational(num - r.num, denom - r.denom);
}
public Rational mul(Rational r) {
return new Rational(num * r.num, denom * r.denom);
}
public Rational div(Rational r) {
return new Rational(num * r.denom, denom * r.num);
}
public int gcd(Rational r) {
int i = 1;
while (i != 0) {
i = denom % r.denom;
denom = r.denom;
r.denom = i;
}
return denom;
}
public String toString() {
String a = num + "/" + denom;
return a;
}
public double doubleValue() {
return (double) doubleValue();
}
public float floatValue() {
return (float) floatValue();
}
public int intValue() {
return (int) intValue();
}
public long longValue() {
return (long) longValue();
}
}
Main.java
public class Main {
public static void main(String[] args) {
Rational a = new Rational(2, 4);
Rational b = new Rational(2, 6);
System.out.println(a + " + " + b + " = " + a.add(b));
System.out.println(a + " - " + b + " = " + a.sub(b));
System.out.println(a + " * " + b + " = " + a.mul(b));
System.out.println(a + " / " + b + " = " + a.div(b));
Rational[] arr = {new Rational(7, 1), new Rational(6, 1),
new Rational(5, 1), new Rational(4, 1),
new Rational(3, 1), new Rational(2, 1),
new Rational(1, 1), new Rational(1, 2),
new Rational(1, 3), new Rational(1, 4),
new Rational(1, 5), new Rational(1, 6),
new Rational(1, 7), new Rational(1, 8),
new Rational(1, 9), new Rational(0, 1)};
selectSort(arr);
for (int i = 0; i < arr.length - 1; ++i) {
if (arr[i].compareTo(arr[i + 1]) > 0) {
System.exit(1);
}
}
Number n = new Rational(3, 2);
System.out.println(n.doubleValue());
System.out.println(n.floatValue());
System.out.println(n.intValue());
System.out.println(n.longValue());
}
public static <T extends Comparable<? super T>> void selectSort(T[] array) {
T temp;
int mini;
for (int i = 0; i < array.length - 1; ++i) {
mini = i;
for (int j = i + 1; j < array.length; ++j) {
if (array[j].compareTo(array[mini]) < 0) {
mini = j;
}
}
if (i != mini) {
temp = array[i];
array[i] = array[mini];
array[mini] = temp;
}
}
}
}
Результат
2/4 + 2/6 = 4/10
Exception in thread "main" java.lang.StackruError
2/4 - 2/6 = 0/-2
at com.xetrasu.Rational.doubleValue(Rational.java:64)
2/4 * 2/6 = 4/24
at com.xetrasu.Rational.doubleValue(Rational.java:64)
2/4 / 2/6 = 12/8
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
at com.xetrasu.Rational.doubleValue(Rational.java:64)
Вот исходный код StackruError
в OpenJDK 7