Избыточный вызов метода уменьшает доступную память стека

Я встретил StackOverflowError в своем реальном проекте и сделал простую модель, которая показывает проблему. Это тестовый класс, который вызывает некоторый рекурсивный метод и сохраняет глубину ошибки.

public class Main {
    static int c = 0;

    public static void main(String[] args) {
        long sum = 0;
        int exps = 100;
        for (int i = 0; i < exps; ++i) {
            c = 0;
            try {
                simpleRecursion();
            } catch (StackOverflowError e) {
                sum += c;
            }
        }
        System.out.println("Average method call depth: " + (sum / exps));
    }

    public static void simpleRecursion() {
        simpleMethod();
        ++c;
        simpleRecursion();
    }
}

Есть две версии simpleMethod:

public static void simpleMethod() {
}
  1. Он получает 51K или 59K вызовов методов в тестах.
public static void simpleMethod() {
    c += 0;
}
  1. Он получает 48K или 58K вызовов методов в тестах.

Почему эти реализации дали разные результаты? Я не могу понять, какие лишние данные лежат в стеке во втором случае. На мой взгляд, simpleMethod не должен влиять на память стека, потому что он не находится в цепочке вызовов.

Где условие, останавливающее рекурсию?

Houssam Badri 26.12.2020 09:53

@HoussemBadri Рекурсия останавливается, когда возникает ошибка переполнения стека.

khelwood 26.12.2020 09:56

В чем смысл цикла for? Просто вызвать simpleRecursion(); недостаточно, чтобы продемонстрировать это?

akuzminykh 26.12.2020 09:58

@khelwood - это желаемое поведение или фактическое поведение (тот факт, что исключение было создано для остановки рекурсии)?

Houssam Badri 26.12.2020 10:03

@HoussemBadri Они намеренно вызывают переполнение стека, чтобы увидеть, насколько глубоким может быть стек.

khelwood 26.12.2020 10:07

Меня интересуют результаты обоих случаев. Что вы подразумеваете под 51К или 59К? Всегда ли это один из них (приблизительно) или диапазон?

Thiyagu 26.12.2020 10:08

@user7 это всегда один из них +- 0.1K

Alexey Shishkin 26.12.2020 10:25

@akuzminykh, я думал, что средний результат более нагляден

Alexey Shishkin 26.12.2020 10:28

Отвечает ли это на ваш вопрос? Есть ли встроенные функции в java?

Progman 26.12.2020 13:05

@Progman, какой эффект встраивания здесь важен?

tgdavies 26.12.2020 13:35

@tgdavies Размер стека 2 (проверьте javap -v) будет добавлен к размеру стека simpleRecursion(), когда метод simpleMethod() будет встроен.

Progman 26.12.2020 13:37

@Progman Интересно, возможно, вам следует дать это в качестве ответа, поскольку я не думаю, что вопрос, на который вы ссылаетесь, объясняет это?

tgdavies 26.12.2020 13:42

@Progman Я считаю, что это может быть встроенный эффект, но не могли бы вы описать, где в этом случае сохраняется память стека? на мой взгляд, в стеке все равно нет упоминания о simpleMethod

Alexey Shishkin 26.12.2020 13:49
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
13
143
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема, с которой вы столкнулись, может быть связана с встраиванием методов JVM из-за соображений производительности. Встраивание метода может повлиять на размер стека, выделенный для этого метода. Вы можете проверить с помощью javap -v, насколько велик размер стека для метода, который выделяется при вызове метода. Для вашего кода результат javap -v выглядит следующим образом:

Метод simpleRecursion():

  public static void simpleRecursion();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: invokestatic  #13                 // Method simpleMethod:()V
         3: getstatic     #2                  // Field c:I
         6: iconst_1
         7: iadd
         8: putstatic     #2                  // Field c:I
        11: invokestatic  #3                  // Method simpleRecursion:()V
        14: return
      LineNumberTable:
        line 19: 0
        line 20: 3
        line 21: 11
        line 22: 14

Метод simpleMethod() без строки c+=0;:

  public static void simpleMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 25: 0

Метод simpleMethod(); со строкой c+=0;:

  public static void simpleMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field c:I
         3: iconst_0
         4: iadd
         5: putstatic     #2                  // Field c:I
         8: return
      LineNumberTable:
        line 25: 0
        line 26: 8

Для варианта метода с пустым телом требуется размер стека 0, тогда как для варианта метода со строкой c+=0; требуется размер стека 2.

Я предполагаю, что когда метод simpleMethod() встраивается в simpleRecursion() с помощью JVM/JIT/HotSpot (см. другие вопросы, такие как Существуют ли встроенные функции в java? или Будут ли встроенные методы Java во время оптимизации?) это увеличит размер стека simpleRecursion(), чтобы освободить место для необходимого дополнительного размера стека simpleMethod(). Теперь размер стека simpleRecursion() больше, что приводит к более раннему достижению лимита с StackOverflowError.

К сожалению, я не могу это проверить, так как задействован JIT/HotSpot. Черт возьми, даже запуск одного и того же приложения несколько раз приводит к разным значениям c в конце. И когда я пробую это с вариантом simpleRecursion(), где c+=0; используется вместо вызова метода simpleMethod();, размер стека остается прежним, скорее всего, потому, что компилятор достаточно умен, чтобы работать с тем же размером стека 2.

Потрясающее объяснение. Спасибо. Если я сделаю simpleMethod достаточно сложным, то есть шанс, что размер стека для simpleRecursion снова уменьшится. Это правда?

Alexey Shishkin 26.12.2020 15:37

Другие вопросы по теме