Stackoverflow что это за ошибка
Перейти к содержимому

Stackoverflow что это за ошибка

  • автор:

Что такое StackOverflow ошибка: раскрываем тайну

Lorem ipsum dolor

В мире программистов ошибка «stack overflow» очень известн а б лагодаря тому, что этот вид ошибки довольно распространен. А сам термин «stack overflow» известен еще больше , чем ошибка, благодаря одноименному англоязычному ресурсу «StackOverflow». Это сообщество программистов международного масштаба , и еще куча всего интересного. Поэтому не нужно путать ошибку « stack overflow » и веб-ресурс с таким же названием. В нашей статье речь пойдет об ошибке.

Ошибка «stack overflow» связана с переполнением стека. Она появляется в том случае, когда в стеке должно сохранит ь ся больше информации, чем он может уместить. Объем памяти стека задает программист при запуске программы. Если в процессе выполнения программы стек переполняется, тогда возникает ошибка « stack overflow » и программа аварийно завершает работу. Причин возникновения подобной ошибки может быть несколько.

Ошибка « stack overflow »

  • бесконечная рекурсия;

  • глубокая рекурсия;

  • проблемы с переменными в стеке.

Бесконечная рекурсия и ошибка «stack overflow»

  • забывает прописывать условие для выхода из рекурсии;

  • пишет неосознанную косвенную рекурсию.

Глубокая рекурсия и ошибка «stack overflow»

  • отыскать другой программный алгоритм для решения поставленной задачи, чтобы избежать применени я рекурсии;

  • «вынести» рекурсию за пределы аппаратного стека в динамический;

  • и другое.

Проблемы с переменными в стеке и ошибка «stack overflow»

Если взглянуть на популярность возникновения «stack overflow error», то причина с проблемными переменными в стеке стоит на первом месте. Кроется она в том, что программист изначально выделяет слишком много памяти локальной переменной.

Например:

int function() <

double b[1000000]

>

В данном случае может возникнуть такая ситуация, что массиву потребуется объем памяти, который стек не способен будет обеспечить, а значит , возникнет ошибка «stack overflow».

Заключение

Ошибка « stack overflow » возникает довольно часто. Каждый конкретный случай ее возникновения требует собственного решения. Одна причина объединяет возникновение такой ошибки — невнимательность программиста. Если « stack overflow error » возникла, значит , программист где-то что-то упустил или не доглядел.

Мы будем очень благодарны

если под понравившемся материалом Вы нажмёте одну из кнопок социальных сетей и поделитесь с друзьями.

Want to learn more about the potential causes and solutions to the StackOverFlowError in your JVM project? Check out this post to learn more.

StackOverFlowError is one of the common confronted JVM errors. In this blog post, we will look at the inner mechanics of thread stacks, reasons that can trigger StackOverFlowError, and potential solutions to address this error.

To gain deeper understanding into StackOverFlowError, let’s review this simple program:

This program is very simple with the following execution code:

  1. main() method is invoked first
  2. main() method invokes a() method. Inside a() method, the integer variable ‘x’ is initialized to value 0.
  3. a() method in turn invokes b() method. Inside b() method, the Car object is constructed and assigned to variable ‘y.'
  4. b() method in turn invokes the c() method. Inside the c() method, the float variable ‘z’ is initialized to value 0.

Now, let’s review what happens behind the scenes when above simple program is executed. Each thread in the application has its own stack. Each stack has multiple stack frames. The thread adds the methods it’s executing, primitive data types, object pointers, and return values to its stack frame in the sequence order in which they are executed.

Fig 1: Thread’s Stack frame

Step #1: main() method is pushed into the application thread’s stack.

Step #2: a() method is 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 is also pushed into the same stack frame. Note that both data, i.e. ‘0’ and variable ‘x,' is pushed into thread’s stack frame.

Step #3: b() method is pushed into thread’s stack. In the b() method, the Car object is created and assigned to variable ‘y.' A crucial point to note here is that the ‘Car’ object is created in the heap and not in the thread’s stack. Only the Car object’s reference, i.e. y, is stored in the thread’s stack frame.

Step #4: c() method is 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 same stack frame. Note both data, i.e. ‘0f’ and variable ‘z,’ is pushed into thread’s stack frame.

Once each method’s execution is completed, then the method and the variables/object pointers are stored in the stack frame are removed, as shown in Fig 2.

Fig 2: Thread’s stack frame after executing methods

What Causes StackOverflowError?

As you can see, thread’s stack is storing methods it’s executing, primitive datatypes, 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 a StackOverflowError :

In this program, the main() method invokes a() method. a() method recursively calls itself. This implementation will cause a() method to be invoked infinite number of times. In this circumstance, a() method will be added to thread’s stack frame infinite number of times. Thus, after a few thousand iterations, thread’s stack size limit would be exceeded. Once stack size limit is exceeded, it will result in StackOverflowError :

Fig 3: StackOverflowError progression

What Are the Solutions to StackOverflowError?

There are couple of strategies to address StackOverflowError .

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 those circumstances, you must fix the source code that 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 the a() method.

2. Increase Thread Stack Size (-Xss)

There might be legitimate reason where a threads stack size needs to be increased. Maybe thread has to execute large number of methods or lot of local variables/created in the methods thread has been executing? In such circumstances, you can increase the thread’s stack size using the JVM argument: ‘-Xss.” This argument needs to be passed 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, and vendor.

Ошибка "Stack overflow" — что это и как ее избежать?

Можете объяснить, когда возникает эта ошибка и как от нее избавиться?

Harry's user avatar

Это означает, что в стеке недостаточно места.

Причины — например, слишком глубокая рекурсия (редко), или слишком большие локальные переменные (куда чаще), или и то и другое сразу 🙂

Как избавиться? Опять же, можно просто в настройках компилятора поднять размер стека.

Но надежнее и лучше — посмотреть, нет ли слишком глубокой (вплоть до бесконечности) рекурсии, заменить локальные массивы на выделяемые динамически.

практически гарантированно даст переполнение стека. В отличие от

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

Harry's user avatar

Про стек и другие типы данных:

  • Стековые данные. На них память выделяется при заходе в процедуру и освобождается при её завершении. Максимальный размер стека программы составляет 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 ошибка!

Недавнее исследование «Использование и атрибуция сниппетов кода Stack Overflow в проектах GitHub» внезапно обнаружило, что чаще всего в опенсорсных проектах встречается мой ответ, написанный почти десять лет назад. По иронии судьбы, там баг.

Давным-давно…

Еще в 2010 году я сидел в своём офисе и занимался ерундой: развлекался код-гольфингом и накручивал рейтинг на Stack Overflow.

Моё внимание привлёк следующий вопрос: как вывести количество байт в удобочитаемом формате? То есть как преобразовать что-то вроде 123456789 байт в «123,5 МБ».


Старый добрый интерфейс 2010 года, спасибо The Wayback Machine

Неявно подразумевалось, что результатом будет число между 1 и 999,9 с соответствующей единицей измерения.

Уже был один ответ с циклом. Идея простая: проверять все степени с самой большой единицы (ЭБ = 10 18 байт) до самой маленькой (Б = 1 байт) и применить первую, которая меньше числа байт. В псевдокоде это выглядит примерно так:

Обычно при наличии правильного ответа с положительной оценкой его трудно догнать. На жаргоне Stack Overflow это называется проблемой самого быстрого стрелка на Западе. Но здесь у ответа было несколько недостатков, поэтому я всё равно надеялся его превзойти. По крайней мере, код с циклом можно значительно сократить.

Это ж алгебра, всё просто!

Тут меня осенило. Приставки кило-, мега-, гига-,… — ни что иное, как степени 1000 (или 1024 в стандарте МЭК), так что правильную приставку можно определить с помощью логарифма, а не цикла.

Основываясь на этой идее, я опубликовал следующее:

Конечно, это не очень читабельно, и log/pow уступает по эффективности другим вариантам. Но никакого цикла и почти нет ветвлений, так что результат получился довольно красивым, на мой взгляд.

Математика тут нехитрая. Количество байт выражается как byteCount = 1000 s , где s представляет степень (в двоичной нотации база 1024.) Решение s дает s = log1000(byteCount).

В API нет простого выражения log1000, но мы можем выразить его в терминах натурального логарифма следующим образом s = log(byteCount) / log(1000). Затем преобразуем s в int, так что если у нас, например, более одного мегабайта (но не полный гигабайт), то в качестве единицы измерения будет использоваться МБ.

Получается, что если s = 1, то используется размерность килобайт, если s = 2 — мегабайт и так далее. Делим byteCount на 1000 s и шлёпаем соответствующую букву в префикс.

Оставалось только подождать и посмотреть, как сообщество воспримет ответ. Я подумать не мог, что этот фрагмент кода станет самым тиражирумым в истории Stack Overflow.

Исследование по атрибуции

Перенесёмся в 2018 год. Аспирант Себастьян Балтес публикует в научном журнале Empirical Software Engineering статью под названием «Использование и атрибуция сниппетов кода Stack Overflow в проектах GitHub». Тема его исследования — насколько соблюдается лицензия Stack Overflow CC BY-SA 3.0, то есть указывают ли авторы ссылки на Stack Overflow как источник кода.

Для анализа из дампа Stack Overflow были извлечены сниппеты кода и сопоставлены с кодом в публичных репозиториях GitHub. Цитата из реферата:

(Спойлер: нет, большинство программистов не соблюдает требования лицензии).

В статье есть такая таблица:

Этот ответ вверху с идентификатором 3758880 оказался тем самым ответом, который я опубликовал восемь лет назад. На данный момент у него более ста тысяч просмотров и более тысячи плюсов.

Быстрый поиск на GitHub действительно выдаёт тысячи репозиториев с кодом humanReadableByteCount .

Поиск этого фрагмента в своём репозитории:

Забавная история, как я узнал об этом исследовании.

Себастьян нашёл совпадение в репозитории OpenJDK без какой-либо атрибуции, а лицензия OpenJDK не совместима с CC BY-SA 3.0. В списке рассылки jdk9-dev он спросил: это код Stack Overflow скопирован из OpenJDK или наоборот?

Самое смешное то, что я как раз работал в Oracle, в проекте OpenJDK, поэтому мой бывший коллега и друг написал следующее:

Почему бы не спросить напрямую у автора этого сообщения на SO (aioobe)? Он является участником OpenJDK и работал в Oracle, когда этот код появился в исходных репозиториях OpenJDK.

Oracle очень серьёзно относится к таким вопросам. Я знаю, что некоторые менеджеры вздохнули с облегчением, когда прочитали этот ответ и нашли «виновника».

Затем Себастьян написал мне, чтобы прояснить ситуацию, что я и сделал: этот код добавили ещё до моего прихода в Oracle и я не имею отношения к коммиту. С Oracle лучше не шутить. Через пару дней после открытия тикета этот код был удалён.

Держу пари, вы уже задумались об этом. Что же за ошибка в коде?

После эксабайтов (10 18 ) идут зеттабайты (10 21 ). Может, действительно большое число выйдет за границы kMGTPE? Нет. Максимальное значение 2 63 -1 ≈ 9,2 × 10 18 , поэтому никакое значение никогда не выйдет за пределы экзабайт.

Может, путаница между единицами СИ и двоичной системой? Нет. В первой версии ответа была путаница, но её исправили довольно быстро.

Может, exp в конечном итоге обнуляется, вызывая сбой charAt(exp-1)? Тоже нет. Первый if-оператор охватывает этот случай. Значение exp всегда будет не менее 1.

Может, какая-то странная ошибка округления в выдаче? Ну вот наконец…

Много девяток

Решение работает до тех пор, пока не приблизится к 1 МБ. Когда в качестве входных данных задано 999 999 байт, результат (в режиме СИ) — «1000,0 kB» . Хотя 999 999 ближе к 1000 × 1000 1 , чем к 999,9 × 1000 1 , сигнификант 1000 запрещён спецификацией. Правильный результат — «1.0 MB» .

В своё оправдание могу сказать, что на момент написания такая ошибка была во всех 22 опубликованных ответах, включая Apache Commons и библиотеки Android.

Как это исправить? Прежде всего, отметим, что показатель степени (exp) должен измениться с ‘k’ на ‘M’, как только число байт ближе к 1 × 1,000 2 (1 МБ), чем к 999,9 × 1000 1 (999,9 k). Это происходит на 999 950. Точно так же следует переключиться с ‘M’ на ‘G’, когда мы проходим 999 950 000 и так далее.

Вычисляем этот порог и увеличиваем exp , если bytes больше:

С этим изменением код работает хорошо до тех пор, пока количество байт не приблизится к 1 ЭБ.

Ещё больше девяток

При расчёте 999 949 999 999 999 999 код выдаёт 1000.0 PB , а правильный результат 999.9 PB . Математически код точен, так что же здесь происходит?

Теперь мы столкнулись с ограничениями double .

Введение в арифметику с плавающей запятой

Согласно спецификации IEEE 754, у близких к нулю значений с плавающей запятой очень плотное представление, а у больших значений — очень разреженное. На самом деле, половина всех значений находится между -1 и 1, а когда речь идёт о больших числах, значение размером Long.MAX_VALUE ничего не значит. В прямом смысле.

Проблему представляют два вычисления:

  • Деление в аргументе String.format и
  • Порог для наращивания exp
Уменьшение промежуточных значений

Для решения первой проблемы можем уменьшить значение bytes до нужного диапазона, где точность лучше, и соответственно настроить exp . Конечный результат в любом случае округляется, поэтому неважно, что мы выбрасываем наименее значимые разряды.

Настройка наименее значимых битов

Для решения второй проблемы нам важны наименее значимые биты (у 99994999. 9 и 99995000. 0 должны быть разные степени), поэтому придётся найти иное решение.

Сначала отметим, что существует 12 различных пороговых значений (по 6 для каждого режима), и только одно из них приводит к ошибке. Неправильный результат можно однозначно идентифицировать, потому что он заканчивается на D0016. Значит, можно исправить его напрямую.

Поскольку в результатах с плавающей запятой мы полагаемся на определённые битовые шаблоны, то применяем модификатор strictfp для гарантии, что код работает независимо от аппаратного обеспечения.

Отрицательные значения на входе

Неясно, при каких обстоятельствах может иметь смысл отрицательное количество байт, но поскольку в Java нет беззнакового long , лучше обработать такой вариант. Прямо сейчас ввод вроде -10 000 выдаёт -10000 B .

Выражение такое многословное, потому что -Long.MIN_VALUE == Long.MIN_VALUE . Теперь мы выполняем все вычисления exp , используя absBytes вместо bytes .

Окончательная версия

Вот окончательная версия кода, сокращённая и уплотнённая в духе оригинальной версии:

Обратите внимание, что это началось как попытка избежать циклов и чрезмерного ветвления. Но после сглаживания всех пограничных ситуаций код стал ещё менее читабельным, чем исходная версия. Лично я бы не стал копировать этот фрагмент в продакшн.

Для обновлённой версии продакшн-качества см. отдельную статью: «Форматирование размера байт в удобочитаемый формат».

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *