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

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

  1. На сколько уровней в стеке находится текущий код?
  2. Сколько уровней рекурсии может достичь текущий метод, прежде чем попадет в StackOverflowError?

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

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

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

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

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

Вопрос:

Is there a better way of doing this that isn't so hacky and doesn't have this limitation?

Как бы то ни было, я предполагаю, что его нет: 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) результаты полностью согласуются: 9923 на моей машине (Ubuntu 14.04 64-бит). Но 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) переполняется.

Вопрос:

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


person chiastic-security    schedule 20.11.2014    source источник
comment
Связанное обсуждение для Java 7: stackoverflow .com / questions / 5165753 / Я знаю, что вы спрашиваете о Java 8, но только ради потерянных посетителей.   -  person Grzegorz Oledzki    schedule 20.11.2014
comment
Просто наблюдение: -Xint не делает его детерминированным, поэтому запуск JIT-компиляции в какой-то момент кажется невозможным. При многократном зондировании максимальной глубины стека на одном и том же экземпляре виртуальной машины (в том же основном потоке) я получаю постоянное максимальное значение; при следующем запуске виртуальной машины (те же аргументы) я получаю немного другое значение max. (Oracle JDK 1.8.0_25)   -  person Durandal    schedule 20.11.2014
comment
Есть ли шанс убедить @BrianGoetz прокомментировать недетерминизм, введенный в Java 8? У новой JVM есть ASLR, Брайан?   -  person chiastic-security    schedule 24.12.2014


Ответы (4)


На наблюдаемое поведение влияет оптимизатор 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

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

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

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

person Holger    schedule 20.11.2014
comment
Да, меня интересовал ASLR. Я нигде не могу найти ничего, что говорило бы, что это не в Java 7, а в Java 8. - person chiastic-security; 20.11.2014
comment
ASLR в основном реализуется операционной системой. Поэтому я ожидал, что начальный адрес стека будет отличаться и в Java 7. Поэтому необходимо изменить размер или конечный адрес. Возможно, в Java 8 добавлена ​​рандомизация размера, чтобы сделать StackOverflows намеренно менее предсказуемым, чтобы усложнить использование ошибок. - person Holger; 21.11.2014

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

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

person Pedro Montoto García    schedule 20.11.2014
comment
Какое значение имеет связанный пост в блоге? - person Durandal; 20.11.2014
comment
Перечисляет различные gc в java 8 - person Pedro Montoto García; 20.11.2014

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

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

Согласно Javadoc,

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

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

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

person Elliott Frisch    schedule 20.11.2014
comment
Но почему сборщик мусора влияет на объем стека в текущем потоке? Разве он не работает в собственном потоке? Я действительно задавался вопросом об этом, поэтому я закодировал это, чтобы избежать создания каких-либо объектов. - person chiastic-security; 20.11.2014

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

новый Throwable (). getStackTrace ()

Or:

Поток.currentThread (). GetStackTrace ()

Это все еще хакерский метод, поскольку результат зависит от реализации JVM. И JVM может решить урезать результат для лучшей производительности.

person Chris    schedule 19.12.2014