Что происходит в стеке?

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

(meta+varaibles) * frames2 = meta * frames1 varaibles я бы контролировал сам, добавляя новые переменные во второй метод

frames1 и frames2 я просто по очереди вывожу на консоль

public static void var1(int count) {
    try {
        var1(count + 1);
    }
    catch (StackOverflowError e){
        System.out.println(count);
    }
}

public static void var2(int count) {
    try {
        //*здесь по задумке должно быть объявление какого-то количества переменных*
        var2(count + 1);
    }
    catch (StackOverflowError e){
        System.out.println(count);
    }
}

Дальше решил проверить, возвращается ли мне 2 раза одно и то же число, когда я вызываю один и тот же метод, но увидел следующее:

public static void main(String... args) {
    var1(0);
    var1(0);
}

Вывод:

23540

1409314092140911409014089

14088

14087

14086

14085

14084

14083

14082

14081

14080

14079

14078

14077

14076

14075

14074

14073

14072

14071

1.Не понимаю, почему ломается вывод(StackOverFlow вызывается больше чем 1 раз?)

2.Даже не смотря на сломанный вывод можно заметить, что во втором случае у меня гораздо меньше свободного места

Объяснения этому найти не могу(есть предположение, что стек по какой-то причине меняет свой размер(?))


Ответы (1 шт):

Автор решения: Roman Konoval

Такой странный вывод объяснить просто. Во время работы System.out.println, уже после того как что-то вывелось (иногда не включая новую строку) случается новый StackOverflowError, который ловится на предыдущем вызове (поэтому числа уменьшаются на единицу).

Вместо вывода в блоке catch делайте return count, ну и если без исключения то тоже возвращайте то, что внутренний вызов вернул. И печатайте результат в main:

public class A {
  public static int var1(int count) {
    try {
      return var1(count + 1);
    } catch (StackOverflowError e) {
      return count;
    }
  }

  public static void main(String... args) {
    for(int i = 0; i < 200; ++i) {
      System.out.println(var1(0));
    }
  }
}

С параметрами по умолчанию на JVM в которой есть JIT, это выводит что-то типа:

19757
23817
23817
23817
23817
... и т.д. еще раз 20
39697
39697
... и дальше без изменений

Но если отключить JIT, то результат становится стабильным (и гораздо хуже):

9923
9923
9923
9923
... и т.д.

На java8 я это сделал добавлением опции -Djava.compiler=NONE при старте JVM. Для других версия смотрите тут https://stackoverflow.com/q/34315960/2231972

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

Причем среда делает это не один раз, а постоянно анализирует исполнение кода, и в моем случае делала это дважды улучшив результат вдвое.

Возвращаясь к вопросу про размер фрейма, это все очень непросто. Одно дело, когда идет чисто интерпретация (когда JIT выключен), другое дело, когда есть еще компиляция (и перекомпиляция). Компилятор оптимизируюющий, да еще в динамике учитывая статистику исполнения и, как видно даже в простом примере, может делать очень сильные оптимизации, которые в том числе влияют на размер фрейма.

→ Ссылка