Подтвердить что ты не робот

Почему максимальная глубина рекурсии я могу достичь недетерминированной?

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

  • Сколько уровней в стеке является текущим кодом?
  • Сколько уровней рекурсии может достичь текущего метода до того, как он достигнет значения StackOverflowError?

Глубина стека текущего исполняемого кода

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

public static int levelsDeep() {
    try {
        throw new SomeKindOfException();
    } catch (SomeKindOfException e) {
        return e.getStackTrace().length;
    }
}

Это кажется немного взломанным. Он генерирует и ловит исключение, а затем смотрит, какая длина трассировки стека.

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

Вопрос:

Есть ли лучший способ сделать это, что не так хаки и не имеет этого ограничения?

Для чего я думаю, что нет: Throwable.getStackTraceDepth() - это родной вызов, который предлагает (но не доказывает), что это невозможно сделать в чистой Java.

Определение того, насколько мы оставили оставшуюся глубину рекурсии

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

Здесь мой код для этого:

public static int stackLeft() {
    try {
        return 1+stackLeft();
    } catch (StackOverflowError e) {
        return 0;
    }
}

Выполняет свою работу превосходно, даже если она линейна в количестве оставшихся стеков. Но вот очень, очень странная часть. На 64-разрядной версии Java 7 (OpenJDK 1.7.0_65) результаты совершенно согласуются: 9,923, на моей машине (Ubuntu 14.04 64-bit). Но Oracle Java 8 (1.8.0_25) дает мне недетерминированные результаты: я получаю записанную глубину где-то между примерно 18 500 и 20 700.

Теперь, почему это было бы недетерминированным? Там должен быть фиксированный размер стека, не так ли? И весь код выглядит детерминированным для меня.

Я задавался вопросом, было ли это что-то странное с улавливанием ошибок, поэтому я попробовал это вместо:

public static long badSum(int n) {
    if (n==0)
        return 0;
    else
        return 1+badSum(n-1);
}

Ясно, что это либо вернет введенный им ввод, либо переполнение.

Опять же, результаты, которые я получаю, не детерминированы на Java 8. Если я назову badSum(14500), он даст мне StackOverflowError примерно в половине случаев и вернет 14500 другую половину. но на Java 7 OpenJDK он согласован: badSum(9160) завершает штраф и badSum(9161) переполнения.

Вопрос:

Почему максимальная глубина рекурсии не детерминирована на Oracle Java 8? И почему он детерминирован на OpenJDK 7?
4b9b3361

Ответ 1

Наблюдаемое поведение зависит от оптимизатора HotSpot, но это не единственная причина. Когда я запускаю следующий код

public static void main(String[] argv) {
    System.out.println(System.getProperty("java.version"));
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
}
static int countDepth() {
    try { return 1+countDepth(); }
    catch(StackOverflowError err) { return 0; }
}

с включенным JIT, я получаю такие результаты, как:

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -cp build\classes X
1.8.0_40-ea
2097
4195
4195
4195
12587
12587
12587

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -cp build\classes X
1.8.0_40-ea
2095
4193
4193
4193
12579
12579
12579

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -cp build\classes X
1.8.0_40-ea
2087
4177
4177
12529
12529
12529
12529

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

Напротив, с отключенным JIT я получаю следующие результаты:

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -Xint -cp build\classes X
1.8.0_40-ea
2104
2104
2104
2104
2104
2104
2104

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -Xint -cp build\classes X
1.8.0_40-ea
2076
2076
2076
2076
2076
2076
2076

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -Xint -cp build\classes X
1.8.0_40-ea
2105
2105
2105
2105
2105
2105
2105

Значения по-прежнему изменяются, но не в пределах одного потока выполнения и с меньшей величиной.

Таким образом, существует (довольно небольшая) разница, которая становится намного больше, если оптимизатор может уменьшить пространство стека, необходимое для вызова метода, например. из-за inlining.

Что может вызвать такую ​​разницу? Я не знаю, как это работает JVM, но один сценарий может заключаться в том, что способ ограничения стека требует определенного выравнивания конечного адреса стека (например, соответствующего размера страницы памяти), в то время как распределение памяти возвращает память с начальным адресом, который имеет более слабая гарантия выравнивания. Объедините такой сценарий с ASLR, и в пределах размера требования выравнивания всегда может быть разница.

Ответ 2

Why is the maximum recursion depth non-deterministic on Oracle Java 8? And why is it deterministic on OpenJDK 7?

Об этом, возможно, относится к изменениям в сборке мусора. Java может выбирать другой режим для gc каждый раз. http://vaskoz.wordpress.com/2013/08/23/java-8-garbage-collectors/

Ответ 3

Это устарело, но вы можете попробовать Thread.countStackFrames(), например

public static int levelsDeep() {
    return Thread.currentThread().countStackFrames();       
}

В Javadoc,

Устаревшие. Определение этого вызова зависит от suspend(), который устарел. Кроме того, результаты этого вызова никогда не были четко определены.

Подсчитывает количество кадров стека в этом потоке. Нить должна быть приостановлена.

Что касается того, почему вы наблюдаете недетерминированное поведение, я могу только предположить, что это некоторая комбинация JIT и сборщика мусора.

Ответ 4

Вам не нужно улавливать исключение, просто создайте его вот так:

new Throwable(). getStackTrace()

Или:

Thread.currentThread(). GetStackTrace()

Он по-прежнему взломан, поскольку результат специфичен для JVM. И JVM может решить обрезать результат для лучшей производительности.