StackOverflowError ב- Java

1. סקירה כללית

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

במאמר זה נראה כיצד שגיאה זו יכולה להתרחש על ידי בחינה במגוון דוגמאות קוד וכן כיצד נוכל להתמודד איתה.

2. ערימת מסגרות ואיך StackOverflowError מתרחשת

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

יצירת מסגרות הערימה תימשך עד שתגיע לסוף קריאות השיטה הנמצאות בשיטות מקוננות.

במהלך תהליך זה, אם JVM נתקל במצב בו אין מקום ליצור מסגרת מחסנית חדשה, היא תזרוק StackOverflowError.

הסיבה השכיחה ביותר ל- JVM להיתקל במצב זה היא רקורסיה בלתי מוגמרת / אינסופית - תיאור Javadoc עבור StackOverflowError מזכיר כי השגיאה נזרקת כתוצאה מחזרה עמוקה מדי בקטע קוד מסוים.

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

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

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

בחלק הבא נבחן כמה דוגמאות קוד המדגימות תרחישים אלה.

3. StackOverflowError בִּפְעוּלָה

בדוגמה המוצגת להלן, א StackOverflowError יושלך בגלל רקורסיה לא מכוונת, שם היזם שכח לציין תנאי סיום להתנהגות הרקורסיבית:

מחלקה ציבורית UnintendedInfiniteRecursion {public int calculatorFactorial (int מספר) {return number * calcentFactorial (number - 1); }}

הנה, השגיאה נזרקת בכל הזדמנות לכל ערך המועבר לשיטה:

מחלקה ציבורית UnintendedInfiniteRecursionManualTest {@Test (צפוי = StackOverflowError.class) חלל ציבורי givenPositiveIntNoOne_whenCalFact_thenThrowsException () {int numToCalcFactorial = 1; UnintendedInfiniteRecursion uir = UnintendedInfiniteRecursion חדש (); uir.calculateFactorial (numToCalcFactorial); } @Test (צפוי = StackOverflowError.class) חלל ציבורי givenPositiveIntGtOne_whenCalcFact_thenThrowsException () {int numToCalcFactorial = 2; UnintendedInfiniteRecursion uir = UnintendedInfiniteRecursion חדש (); uir.calculateFactorial (numToCalcFactorial); } @Test (צפוי = StackOverflowError.class) חלל ציבורי שניתן NegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; UnintendedInfiniteRecursion uir = UnintendedInfiniteRecursion חדש (); uir.calculateFactorial (numToCalcFactorial); }}

עם זאת, בדוגמה הבאה מצוין תנאי סיום אך לעולם אינו מתקיים אם ערך של -1 מועבר ל calculatorFactor () שיטה, הגורמת לרקורסיה בלתי מוגמרת / אינסופית:

class public InfiniteRecursionWithTerminationCondition {public int calculatorFactorial (int number) {return number == 1? 1: מספר * calcFactorial (מספר - 1); }}

מערך הבדיקות הזה מדגים תרחיש זה:

class public InfiniteRecursionWithTerminationConditionManualTest {@Test public void givenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition (); assertEquals (1, irtc.calculateFactorial (numToCalcFactorial)); } @Test public void givenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = new InfiniteRecursionWithTerminationCondition (); assertEquals (120, irtc.calculateFactorial (numToCalcFactorial)); } @Test (צפוי = StackOverflowError.class) חלל ציבורי שניתן NegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = InfiniteRecursionWithTerminationCondition חדש (); irtc.calculateFactorial (numToCalcFactorial); }}

במקרה ספציפי זה, ניתן היה למנוע את השגיאה לחלוטין אם תנאי הסיום פשוט נקבע כ:

class class RecursionWithCorrectTerminationCondition {public int calculateFactorial (int number) {return number <= 1? 1: מספר * calcFactorial (מספר - 1); }}

הנה המבחן שמראה תרחיש זה בפועל:

Class class RecursionWithCorrectTerminationConditionManualTest {@Test public void givenNegativeInt_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = חדש RecursionWithCorrectTerminationCondition (); assertEquals (1, rctc.calculateFactorial (numToCalcFactorial)); }}

עכשיו בואו נסתכל על תרחיש שבו StackOverflowError קורה כתוצאה מיחסים מחזוריים בין כיתות. בואו נשקול ClassOne ו ClassTwo, אשר מייצרים זה את זה בתוך הבונים שלהם וגורמים למערכת יחסים מחזורית:

ClassOne בכיתה ציבורית {private int oneValue; פרטי ClassTwo clsTwoInstance = null; ClassOne ציבורי () {oneValue = 0; clsTwoInstance = ClassTwo חדש (); } ClassOne ציבורי (int oneValue, ClassTwo clsTwoInstance) {this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; }}
מחלקה ציבורית ClassTwo {private int twoValue; פרטי ClassOne clsOneInstance = null; ClassTwo () {twoValue = 10 ציבורי; clsOneInstance = ClassOne חדש (); } ClassTwo ציבורי (int twoValue, ClassOne clsOneInstance) {this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; }}

בואו נגיד שאנחנו מנסים ליצור אינסטנט ClassOne כפי שנראה במבחן זה:

מחלקה ציבורית CyclicDependancyManualTest {@Test (צפוי = StackOverflowError.class) חלל ציבורי כאשרInstanciatingClassOne_thenThrowsException () {ClassOne obj = ClassOne חדש (); }}

זה בסופו של דבר עם StackOverflowError מאז הקונסטרוקטור של ClassOne מיידית ClassTwo, והבנאי של ClassTwo שוב מיידית ClassOne. וזה קורה שוב ושוב עד שהוא עולה על גדותיו.

לאחר מכן, נבחן מה קורה כאשר מחלקה מופעלת באותה מחלקה כמשתנה מופע של אותה מחלקה.

כפי שנראה בדוגמה הבאה, בעל החשבון מיידית את עצמה כמשתנה מופע jointAccountHolder:

מחלקה ציבורית AccountHolder {פרטי מחרוזת שם פרטי; שם משפחה פרטי מחרוזת; AccountHolder jointAccountHolder = AccountHolder חדש (); }

כאשר בעל החשבון הכיתה היא מיידית, א StackOverflowError נזרק עקב קריאתו הרקורסיבית של הבנאי כפי שנראה במבחן זה:

מחלקה ציבורית AccountHolderManualTest {@Test (צפוי = StackOverflowError.class) חלל ציבורי כאשרInstanciatingAccountHolder_thenThrowsException () {מחזיק AccountHolder = AccountHolder חדש (); }}

4. התמודדות עם StackOverflowError

הדבר הטוב ביותר לעשות כאשר א StackOverflowError נתקל הוא לבדוק את עקבות הערימה בזהירות כדי לזהות את הדפוס החוזר של מספרי קווים. זה יאפשר לנו לאתר את הקוד שיש לו רקורסיה בעייתית.

בואו נבדוק כמה עקבות מחסנית שנגרמו על ידי דוגמאות הקוד שראינו קודם.

עקבות הערימה הזו מיוצרות על ידי InfiniteRecursionWithTerminationConditionManualTest אם נשמיט את צָפוּי הצהרת חריג:

java.lang.StackOverflowError ב cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) בשעה cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) בשעה cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) בשעה cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java : 5)

כאן ניתן לראות שורה מספר 5 חוזרת. כאן מתבצעת השיחה הרקורסיבית. עכשיו זה רק עניין של בחינת הקוד כדי לראות אם הרקורסיה נעשית בצורה נכונה.

הנה עקבות הערימה שאנו מקבלים על ידי ביצוע CyclicDependancyManualTest (שוב, בלי צָפוּי יוצא מן הכלל):

java.lang.StackOverflowError at c.b.s.ClassTwo. (ClassTwo.java:9) at c.b.s.ClassOne. (ClassOne.java:9) at c.b.s. ClassTwo. (ClassTwo.java:9) at c.b.s. ClassOne. (ClassOne.java:9)

עקבות מחסנית זו מציגות את מספרי השורות הגורמים לבעיה בשתי המחלקות שנמצאות במערכת יחסים מחזורית. קו מספר 9 של ClassTwo ושורה מספר 9 של ClassOne להצביע על המיקום בתוך הקונסטרוקטור שבו הוא מנסה לייצר את הכיתה השנייה.

לאחר בדיקת הקוד באופן יסודי ואם אף אחת מהפעולות הבאות (או כל שגיאת לוגיקה אחרת בקוד) אינה הסיבה לשגיאה:

  • רקורסיה מיושמת באופן שגוי (כלומר ללא תנאי סיום)
  • תלות מחזורית בין כיתות
  • מייצב כיתה באותה מחלקה כמשתנה מופע של אותה מחלקה

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

ה -Xss ניתן להשתמש בדגל כדי להגדיל את גודל הערימה, מתצורת הפרויקט או משורת הפקודה.

5. מסקנה

במאמר זה בדקנו מקרוב את ה- StackOverflowError כולל איך קוד Java יכול לגרום לזה ואיך אנחנו יכולים לאבחן ולתקן אותו.

קוד המקור הקשור למאמר זה ניתן למצוא באתר GitHub.