Диагностициране и разрешаване на StackOverflowError

Неотдавнашно съобщение на форума на общността на JavaWorld (Stack Overflow след създаване на нов обект) ми напомни, че основите на StackOverflowError не винаги се разбират добре от хората, които са нови в Java. За щастие StackOverflowError е една от най-лесните грешки по време на изпълнение за отстраняване на грешки и в тази публикация в блога ще покажа колко лесно е да се диагностицира StackOverflowError. Имайте предвид, че потенциалът за препълване на стека не е ограничен до Java.

Диагностицирането на причината за StackOverflowError може да бъде доста лесно, ако кодът е компилиран с включена опция за отстраняване на грешки, така че номерата на редовете да са налични в резултата от проследяването на стека. В такива случаи обикновено става въпрос просто за намиране на повтарящия се модел на номера на редове в стека на стека. Моделът на повтаряне на номера на редове е полезен, защото StackOverflowError често се причинява от неустановена рекурсия. Номерата на повтарящите се линии показват кода, който се извиква пряко или косвено рекурсивно. Имайте предвид, че има ситуации, различни от неограничена рекурсия, при която може да възникне препълване на стека, но тази публикация в блога е ограничена до StackOverflowErrorпричинена от неограничена рекурсия.

Връзката на рекурсията се StackOverflowErrorе повредила е отбелязана в описанието на Javadoc за StackOverflowError, което гласи, че тази грешка е „Хвърлена при преливане на стека, защото приложение се повтаря твърде дълбоко“. Важно е, че StackOverflowErrorзавършва с думата Грешка и е Грешка (разширява java.lang.Error чрез java.lang.VirtualMachineError), а не проверено изключение или изпълнение. Разликата е значителна. В Errorи Exceptionвсеки от тях е специализирана Може да се изхвърли, но предназначението им работа е съвсем различна. Урокът за Java посочва, че грешките обикновено са външни за приложението на Java и по този начин обикновено не могат и не трябва да бъдат улавяни или обработвани от приложението.

Ще покажа, че се StackOverflowErrorсблъсквате с неограничена рекурсия с три различни примера. Кодът, използван за тези примери, се съдържа в три класа, първият от които (и основният клас) е показан след това. Изброявам и трите класа в тяхната цялост, защото номерата на редовете са значителни при отстраняване на грешки в StackOverflowError.

StackOverflowErrorDemonstrator.java

package dustin.examples.stackoverflow; import java.io.IOException; import java.io.OutputStream; /** * This class demonstrates different ways that a StackOverflowError might * occur. */ public class StackOverflowErrorDemonstrator { private static final String NEW_LINE = System.getProperty("line.separator"); /** Arbitrary String-based data member. */ private String stringVar = ""; /** * Simple accessor that will shown unintentional recursion gone bad. Once * invoked, this method will repeatedly call itself. Because there is no * specified termination condition to terminate the recursion, a * StackOverflowError is to be expected. * * @return String variable. */ public String getStringVar() { // // WARNING: // // This is BAD! This will recursively call itself until the stack // overflows and a StackOverflowError is thrown. The intended line in // this case should have been: // return this.stringVar; return getStringVar(); } /** * Calculate factorial of the provided integer. This method relies upon * recursion. * * @param number The number whose factorial is desired. * @return The factorial value of the provided number. */ public int calculateFactorial(final int number) { // WARNING: This will end badly if a number less than zero is provided. // A better way to do this is shown here, but commented out. //return number <= 1 ? 1 : number * calculateFactorial(number-1); return number == 1 ? 1 : number * calculateFactorial(number-1); } /** * This method demonstrates how unintended recursion often leads to * StackOverflowError because no termination condition is provided for the * unintended recursion. */ public void runUnintentionalRecursionExample() { final String unusedString = this.getStringVar(); } /** * This method demonstrates how unintended recursion as part of a cyclic * dependency can lead to StackOverflowError if not carefully respected. */ public void runUnintentionalCyclicRecusionExample() { final State newMexico = State.buildState("New Mexico", "NM", "Santa Fe"); System.out.println("The newly constructed State is:"); System.out.println(newMexico); } /** * Demonstrates how even intended recursion can result in a StackOverflowError * when the terminating condition of the recursive functionality is never * satisfied. */ public void runIntentionalRecursiveWithDysfunctionalTermination() { final int numberForFactorial = -1; System.out.print("The factorial of " + numberForFactorial + " is: "); System.out.println(calculateFactorial(numberForFactorial)); } /** * Write this class's main options to the provided OutputStream. * * @param out OutputStream to which to write this test application's options. */ public static void writeOptionsToStream(final OutputStream out) { final String option1 = "1. Unintentional (no termination condition) single method recursion"; final String option2 = "2. Unintentional (no termination condition) cyclic recursion"; final String option3 = "3. Flawed termination recursion"; try { out.write((option1 + NEW_LINE).getBytes()); out.write((option2 + NEW_LINE).getBytes()); out.write((option3 + NEW_LINE).getBytes()); } catch (IOException ioEx) { System.err.println("(Unable to write to provided OutputStream)"); System.out.println(option1); System.out.println(option2); System.out.println(option3); } } /** * Main function for running StackOverflowErrorDemonstrator. */ public static void main(final String[] arguments) { if (arguments.length < 1) { System.err.println( "You must provide an argument and that single argument should be"); System.err.println( "one of the following options:"); writeOptionsToStream(System.err); System.exit(-1); } int option = 0; try { option = Integer.valueOf(arguments[0]); } catch (NumberFormatException notNumericFormat) { System.err.println( "You entered an non-numeric (invalid) option [" + arguments[0] + "]"); writeOptionsToStream(System.err); System.exit(-2); } final StackOverflowErrorDemonstrator me = new StackOverflowErrorDemonstrator(); switch (option) { case 1 : me.runUnintentionalRecursionExample(); break; case 2 : me.runUnintentionalCyclicRecusionExample(); break; case 3 : me.runIntentionalRecursiveWithDysfunctionalTermination(); break; default : System.err.println("You provided an unexpected option [" + option + "]"); } } } 

Класът по-горе демонстрира три вида неограничена рекурсия: случайна и напълно непредвидена рекурсия, неволна рекурсия, свързана с умишлено циклични връзки, и планирана рекурсия с недостатъчно условие на прекратяване. Всеки от тях и техните резултати се обсъждат по-нататък.

Напълно непреднамерена рекурсия

Може да има моменти, когато се появява рекурсия без никакво намерение за това. Честа причина може да е случайно извикване на метод. Например, не е твърде трудно да станете малко невнимателни и да изберете първата препоръка на IDE за възвръщаема стойност за метод "get", който може да се окаже извикване на същия този метод! Това всъщност е примерът, показан в класа по-горе. В getStringVar()метода неколкократно нарича себе си, докато StackOverflowErrorсе среща. Резултатът ще се появи, както следва:

Exception in thread "main" java.lang.StackOverflowError at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at dustin.examples.stackoverflow.StackOverflowErrorDemonstrator.getStringVar(StackOverflowErrorDemonstrator.java:34) at 

Проследяването на стека, показано по-горе, всъщност е в пъти по-дълго от това, което поставих по-горе, но това е просто същият повтарящ се модел. Тъй като моделът се повтаря, лесно е да се диагностицира, че ред 34 от класа е причинителят на проблеми. Когато разглеждаме този ред, виждаме, че действително това е твърдението, return getStringVar()което в крайна сметка се извиква многократно. В този случай можем бързо да осъзнаем, че предвиденото поведение е било вместо това return this.stringVar;.

Неволна рекурсия с циклични отношения

Съществуват определени рискове от наличието на циклични взаимоотношения между класовете. Един от тези рискове е по-голямата вероятност да се сблъскате с неволна рекурсия, при която цикличните зависимости непрекъснато се извикват между обектите, докато стекът препълни. За да демонстрирам това, използвам още два класа. В Stateкласа и Cityкласа има цикличен relationshiop защото Stateнапример има позоваване на неговия капитал Cityи Cityима препратка към State, в която се намира.

Държава.java

package dustin.examples.stackoverflow; /** * A class that represents a state and is intentionally part of a cyclic * relationship between City and State. */ public class State { private static final String NEW_LINE = System.getProperty("line.separator"); /** Name of the state. */ private String name; /** Two-letter abbreviation for state. */ private String abbreviation; /** City that is the Capital of the State. */ private City capitalCity; /** * Static builder method that is the intended method for instantiation of me. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. * @param newCapitalCityName Name of capital city. */ public static State buildState( final String newName, final String newAbbreviation, final String newCapitalCityName) { final State instance = new State(newName, newAbbreviation); instance.capitalCity = new City(newCapitalCityName, instance); return instance; } /** * Parameterized constructor accepting data to populate new instance of State. * * @param newName Name of newly instantiated State. * @param newAbbreviation Two-letter abbreviation of State. */ private State( final String newName, final String newAbbreviation) { this.name = newName; this.abbreviation = newAbbreviation; } /** * Provide String representation of the State instance. * * @return My String representation. */ @Override public String toString() { // WARNING: This will end badly because it calls City's toString() // method implicitly and City's toString() method calls this // State.toString() method. return "StateName: " + this.name + NEW_LINE + "StateAbbreviation: " + this.abbreviation + NEW_LINE + "CapitalCity: " + this.capitalCity; } } 

City.java