Увеличение размера heap не устраняет StackOverflowError при глубокой рекурсии. Какой ресурс JVM ограничивает глубину вызовов?
Глубину рекурсии ограничивает стек конкретного потока, а не Java heap. Каждый вызов метода создаёт кадр стека; когда доступное пространство стека исчерпывается, JVM выбрасывает StackOverflowError.
Параметр -Xmx изменяет максимальный размер heap и не увеличивает стек потоков. Для изменения размера стека обычно используется -Xss, но чрезмерное увеличение этого параметра ограничивает максимальное число одновременно работающих потоков.
Вызовы методов требуют быстрого хранения адреса возврата, локальных переменных, операндов и другой информации, относящейся к конкретному выполнению. Для этого JVM использует отдельный стек у каждого потока, что изолирует состояние вызовов разных потоков.
Heap предназначен главным образом для объектов, время жизни которых может быть произвольным. Разделение heap и стеков позволяет по-разному управлять памятью: сборщик мусора анализирует объекты heap, тогда как завершение кадров стека происходит при возврате из методов.
Рекурсивный метод создаёт новый кадр при каждом вызове. Если базовое условие отсутствует или достигается слишком поздно, число кадров растёт до исчерпания зарезервированного пространства стека.
Попытка решить проблему увеличением heap не сработает: heap и стеки потоков являются разными областями памяти. Увеличение -Xss может отложить ошибку, но не исправляет бесконечную рекурсию и уменьшает объём памяти, доступный для создания новых потоков.
При вызове метода JVM помещает в стек потока новый кадр. В нём могут находиться локальные переменные, часть операндов, данные для возврата и служебная информация; конкретная структура и размер кадра зависят от JVM, архитектуры, режима выполнения и оптимизаций JIT.
При рекурсии кадры предыдущих вызовов остаются активными, поэтому не могут быть освобождены до возврата из соответствующих методов. Когда JVM не может создать очередной кадр, она сигнализирует о переполнении стека через StackOverflowError.
Минимальный пример механизма:
Число успешно созданных кадров не является переносимой константой: оно зависит от JVM, платформы, компиляции и размера кадра. Поэтому измеренное значение глубины нельзя использовать как универсальный лимит.
Параметр -Xss задаёт размер стека потока или влияет на него в зависимости от JVM и способа запуска. Увеличение стека полезно для обоснованно глубокой, но конечной рекурсии; для неограниченной рекурсии правильное решение — исправить условие завершения или заменить рекурсию явной структурой данных в heap.
У каждого потока есть собственный стек, поэтому суммарное потребление памяти при большом количестве потоков включает множество стеков. Размер стека — это не обязательно объём физической памяти, немедленно занятый целиком, но слишком большой резерв всё равно может ограничить масштабирование и привести к нехватке нативной памяти.
Сервис разбирал вложенные документы рекурсивным методом. Для редких документов с очень большой глубиной вложенности он получал StackOverflowError, хотя heap был заполнен лишь наполовину.
Рассматривались два варианта. Увеличение -Xss требовало меньше изменений, но сохраняло зависимость от глубины входных данных и повышало потребление памяти на поток. Увеличение -Xmx не влияло на причину ошибки.
Выбрали ограничение допустимой глубины с понятной ошибкой для клиента и итеративный обход с явным стеком объектов в heap. Это устранило зависимость от стека JVM и позволило контролировать память, затрачиваемую на обработку одного документа.
Вопрос: Всегда ли каждая рекурсивная активация метода занимает отдельный физический кадр стека?
Ответ: Нет, это практическое упрощение. Интерпретатор и JIT могут представлять кадры по-разному, а оптимизации способны устранять или преобразовывать часть промежуточного состояния. Однако для обычной неограниченной рекурсии активные вызовы требуют сохраняемого состояния, поэтому рассчитывать на устранение всех кадров нельзя.
Вопрос: Почему после перехода от рекурсии к явному стеку ошибка всё равно может быть связана с памятью?
Ответ: Явный стек обычно хранится в heap и содержит ссылки или записи для необработанных узлов. Он не вызывает StackOverflowError, но может расти до OutOfMemoryError, если входные данные слишком велики или алгоритм удерживает лишние объекты. Поэтому нужно контролировать максимальную глубину, размер очереди или стека и время жизни элементов.
Вопрос: Почему увеличение -Xss может ухудшить работу приложения с большим числом потоков?
Ответ: Каждый поток нуждается в собственном стеке. При увеличении размера стека уменьшается потенциальное число потоков, которое процесс может поддерживать в доступной нативной памяти; дополнительно остаются расходы на структуры потоков и другие нативные области JVM. Поэтому -Xss выбирают по реальной глубине вызовов, а не увеличивают без ограничения.