Dijagnosticiranje i rješavanje StackOverflowError

Nedavna poruka foruma JavaWorld zajednice (Stack Overflow nakon instanciranja novog objekta) podsjetila me je da ljudi koji koriste Java ne razumiju uvijek osnove StackOverflowError-a. Srećom, StackOverflowError je jedna od jednostavnih pogrešaka u izvršavanju za otklanjanje pogrešaka, a u ovom objavljivanju na blogu pokazat ću kako je lako dijagnosticirati StackOverflowError. Imajte na umu da potencijal preljeva steka nije ograničen na Javu.

Dijagnosticiranje uzroka StackOverflowError može biti prilično jednostavno ako je kod kompiliran s uključenom opcijom otklanjanja pogrešaka tako da su brojevi redaka dostupni u rezultirajućem praćenju stoga. U takvim slučajevima obično je jednostavno pronaći ponavljajući obrazac brojeva linija u tragu stoga. Uzorak ponavljanja brojeva redaka koristan je jer je StackOverflowError često uzrokovana nedodijeljenom rekurzijom. Brojevi redova koji se ponavljaju označavaju kôd koji se izravno ili neizravno rekurzivno poziva. Imajte na umu da postoje i druge situacije, osim neograničene rekurzije, u kojima se može dogoditi preljev snopa, ali ovo objavljivanje bloga ograničeno je na StackOverflowErroruzrokovanu neograničenom rekurzijom.

Loš odnos rekurzije StackOverflowErrorzabilježen je u opisu Javadoc-a za StackOverflowError koji navodi da je ova pogreška "Bačena kada se dogodi prelijevanje steka jer se aplikacija preduboko ponavlja." Značajno je da StackOverflowErrorzavršava s riječju Greška i da je Greška (proširuje java.lang.Error putem java.lang.VirtualMachineError), a ne provjerena ili runtime iznimka. Razlika je značajna. ErrorI Exceptionsvaki specijalizirana Za odbaciti, ali je njihova namjera rukovanje je sasvim drugačija. Java Tutorial ističe da su pogreške u pravilu vanjske za Java aplikaciju te ih stoga aplikacija obično ne može niti treba hvatati ili rješavati.

Pokazat ću StackOverflowErrornalet preko neograničene rekurzije s tri različita primjera. Kôd korišten za ove primjere sadržan je u tri klase, od kojih je prva (i glavna klasa) prikazana sljedeća. Naveo sam sve tri klase u cijelosti jer su brojevi linija značajni pri otklanjanju pogrešaka 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 + "]"); } } } 

Gornja klasa pokazuje tri vrste neograničene rekurzije: slučajnu i potpuno nenamjernu rekurziju, nenamjernu rekurziju povezanu s namjerno cikličkim odnosima i namjeravanu rekurziju s nedovoljnim završnim uvjetima. O svakom od njih i o njihovim rezultatima govori se dalje.

Potpuno nenamjerna rekurzija

Mogu biti trenuci kada se rekurzija dogodi bez ikakve namjere. Čest uzrok može biti slučajno pozivanje same metode. Na primjer, nije previše teško postati malo neoprezan i odabrati prvu preporuku IDE-a za povratnu vrijednost za "get" metodu koja bi na kraju mogla biti poziv na istu tu metodu! To je zapravo primjer prikazan u gornjoj klasi. getStringVar()Metoda puta sebe naziva dok StackOverflowErrorje naišao. Izlaz će se pojaviti na sljedeći način:

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 

Gore prikazan trag stoga zapravo je mnogo puta duži od onoga koji sam stavio gore, ali to je jednostavno isti ponavljajući obrazac. Budući da se obrazac ponavlja, lako je dijagnosticirati da je linija 34 klase uzročnik problema. Kad pogledamo tu liniju, vidimo da je doista ta izjava return getStringVar()koja se na kraju neprestano naziva. U ovom slučaju možemo brzo shvatiti da je namjeravano ponašanje umjesto toga return this.stringVar;.

Nenamjerna rekurzija s cikličkim odnosima

Postoje određeni rizici da se uspostave ciklični odnosi između klasa. Jedan od tih rizika je veća vjerojatnost da naiđe na nenamjernu rekurziju gdje se cikličke ovisnosti neprestano pozivaju između objekata sve dok se stog ne prelije. Da bih to pokazao, koristim još dva razreda. StateKlasa i Cityklasa imaju cikličku relationshiop jer Stateinstanca ima referencu na njegov glavni grad Cityi Cityima se upućuje na Stateu kojoj se nalazi.

Država.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; } } 

Grad.java