Я встретил 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() {
}
public static void simpleMethod() {
c += 0;
}
Почему эти реализации дали разные результаты? Я не могу понять, какие лишние данные лежат в стеке во втором случае. На мой взгляд, simpleMethod не должен влиять на память стека, потому что он не находится в цепочке вызовов.
@HoussemBadri Рекурсия останавливается, когда возникает ошибка переполнения стека.
В чем смысл цикла for? Просто вызвать simpleRecursion();
недостаточно, чтобы продемонстрировать это?
@khelwood - это желаемое поведение или фактическое поведение (тот факт, что исключение было создано для остановки рекурсии)?
@HoussemBadri Они намеренно вызывают переполнение стека, чтобы увидеть, насколько глубоким может быть стек.
Меня интересуют результаты обоих случаев. Что вы подразумеваете под 51К или 59К? Всегда ли это один из них (приблизительно) или диапазон?
@user7 это всегда один из них +- 0.1K
@akuzminykh, я думал, что средний результат более нагляден
Отвечает ли это на ваш вопрос? Есть ли встроенные функции в java?
@Progman, какой эффект встраивания здесь важен?
@tgdavies Размер стека 2
(проверьте javap -v
) будет добавлен к размеру стека simpleRecursion()
, когда метод simpleMethod()
будет встроен.
@Progman Интересно, возможно, вам следует дать это в качестве ответа, поскольку я не думаю, что вопрос, на который вы ссылаетесь, объясняет это?
@Progman Я считаю, что это может быть встроенный эффект, но не могли бы вы описать, где в этом случае сохраняется память стека? на мой взгляд, в стеке все равно нет упоминания о simpleMethod
Проблема, с которой вы столкнулись, может быть связана с встраиванием методов 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 снова уменьшится. Это правда?
Где условие, останавливающее рекурсию?