Diagnozowanie i rozwiązywanie StackOverflowError

Niedawna wiadomość na forum społeczności JavaWorld (Stack Overflow po utworzeniu instancji nowego obiektu) przypomniała mi, że podstawy StackOverflowError nie zawsze są dobrze rozumiane przez osoby, które nie znają Javy. Na szczęście StackOverflowError jest jednym z najłatwiejszych do debugowania błędów środowiska uruchomieniowego. W tym poście na blogu pokażę, jak łatwo jest zdiagnozować błąd StackOverflowError. Należy zauważyć, że możliwość przepełnienia stosu nie ogranicza się do języka Java.

Diagnozowanie przyczyny StackOverflowError może być dość proste, jeśli kod został skompilowany z włączoną opcją debugowania, dzięki czemu numery wierszy są dostępne w wynikowym śladzie stosu. W takich przypadkach zwykle chodzi po prostu o znalezienie powtarzającego się wzoru numerów wierszy w śladzie stosu. Wzorzec powtarzania numerów wierszy jest pomocny, ponieważ błąd StackOverflowError jest często powodowany przez niezakończoną rekursję. Powtarzające się numery wierszy wskazują kod, który jest bezpośrednio lub pośrednio wywoływany rekurencyjnie. Zwróć uwagę, że istnieją sytuacje inne niż nieograniczona rekurencja, w których może wystąpić przepełnienie stosu, ale ten post na blogu jest ograniczony do StackOverflowErrorspowodowanego nieograniczoną rekurencją.

Zależność rekurencji poszła źle, StackOverflowErrorjest odnotowana w opisie Javadoc dla StackOverflowError, który stwierdza, że ​​ten błąd to „Zgłaszany, gdy występuje przepełnienie stosu, ponieważ aplikacja powtarza się zbyt głęboko”. Znaczące jest, że StackOverflowErrorkończy się słowem Error i jest błędem (rozszerza java.lang.Error poprzez java.lang.VirtualMachineError), a nie sprawdzonym lub runtime Exception. Różnica jest znacząca. ErrorI Exceptionsą każdy specjalizuje Throwable, ale ich zamierzone obchodzenie jest zupełnie inna. Samouczek języka Java wskazuje, że błędy są zwykle zewnętrzne w stosunku do aplikacji Java i dlatego zwykle nie mogą i nie powinny być przechwytywane ani obsługiwane przez aplikację.

Zademonstruję wpadanie na StackOverflowErrornieograniczoną rekurencję na trzech różnych przykładach. Kod użyty w tych przykładach jest zawarty w trzech klasach, z których pierwsza (i główna) jest pokazana jako następna. Podaję wszystkie trzy klasy w całości, ponieważ numery wierszy są istotne podczas debugowania 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 + "]"); } } } 

Powyższa klasa przedstawia trzy typy nieograniczonej rekursji: przypadkową i całkowicie niezamierzoną rekursję, niezamierzoną rekursję związaną z celowo cyklicznymi relacjami oraz zamierzoną rekursję z niewystarczającym warunkiem zakończenia. Każdy z nich i ich wyniki są omówione poniżej.

Całkowicie niezamierzona rekursja

Może się zdarzyć, że rekursja nastąpi bez jakiegokolwiek zamiaru. Częstą przyczyną może być przypadkowe wywołanie samej metody. Na przykład, nie jest zbyt trudne, aby stać się trochę zbyt nieostrożnym i wybrać pierwsze zalecenie IDE dotyczące wartości zwracanej dla metody „get”, która może zakończyć się wywołaniem tej samej metody! W rzeczywistości jest to przykład przedstawiony w klasie powyżej. getStringVar()Metoda nazywa się wielokrotnie, aż do StackOverflowErrornapotkania. Wynik będzie wyglądał następująco:

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 

Pokazany powyżej ślad stosu jest w rzeczywistości wielokrotnie dłuższy niż ten, który umieściłem powyżej, ale jest to po prostu ten sam powtarzający się wzór. Ponieważ wzorzec się powtarza, łatwo jest zdiagnozować, że wiersz 34 tej klasy jest przyczyną problemu. Kiedy patrzymy na tę linię, widzimy, że rzeczywiście jest to stwierdzenie, return getStringVar()które w końcu wielokrotnie się nazywa. W takim przypadku możemy szybko zorientować się, że zamiast tego zamierzano zachować return this.stringVar;.

Niezamierzona rekursja z cyklicznymi relacjami

Istnieją pewne zagrożenia związane z cyklicznymi relacjami między klasami. Jednym z tych zagrożeń jest większe prawdopodobieństwo wystąpienia niezamierzonej rekursji, w której cykliczne zależności są wywoływane bez przerwy między obiektami, aż do przepełnienia stosu. Aby to zademonstrować, używam jeszcze dwóch klas. StateKlasa i Cityklasa posiada cykliczną relationshiop ponieważ Stateinstancja ma odniesienie do kapitału Cityi Cityma odniesienie do State, w którym się on znajduje.

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