אבחון ופתרון StackOverflowError

הודעת פורום קהילת JavaWorld שפורסמה לאחרונה (Stack Overflow לאחר הפעלת אובייקט חדש) הזכירה לי כי היסודות של StackOverflowError לא תמיד מבינים היטב אנשים חדשים ב- Java. למרבה המזל, StackOverflowError הוא אחת הקלות מבין שגיאות זמן הריצה לביצוע באגים ובפרסום בבלוג זה אדגים כמה קל לעיתים קרובות לאבחן StackOverflowError. שים לב שהפוטנציאל של הצפת מחסנית אינו מוגבל ל- Java.

אבחון הסיבה ל- StackOverflowError יכול להיות פשוט למדי אם הקוד נערך עם אפשרות ניפוי הבאגים מופעלת כך שמספרי שורות זמינים במעקב הערימה שהתקבל. במקרים כאלה, זה בדרך כלל פשוט עניין של מציאת התבנית החוזרת של מספרי קווים במסלול הערימה. הדפוס של חזרה על מספרי שורות מועיל מכיוון ש- StackOverflowError נגרם לעיתים קרובות על ידי רקורסיה ללא הפסק. מספרי השורות החוזרות ונשנות מציינים את הקוד הנקרא באופן ישיר או עקיף רקורסיבי. שים לב שישנם מצבים אחרים מלבד רקורסיה בלתי מוגבלת שבהם עלול להתרחש הצפת מחסנית, אך פרסום בבלוג זה מוגבל StackOverflowErrorלנגרם על ידי רקורסיה בלתי מוגבלת.

מערכת היחסים של רקורסיה גרועה StackOverflowErrorמצוינת בתיאור Javadoc עבור StackOverflowError הקובע כי שגיאה זו היא "נזרק כאשר מתרחשת הצפת מחסנית מכיוון שיישום חוזר עמוק מדי." זה משמעותי StackOverflowErrorשמסתיים במילה Error והוא שגיאה (מאריך 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 על ערך החזר לשיטת "קבל" שעלולה בסופו של דבר להיות קריאה לאותה שיטה ממש! זו למעשה הדוגמה המוצגת בכיתה לעיל. 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שבה הוא ממוקם.

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