De StackOverflowError in Java

1. Overzicht

StackOverflowError kan vervelend zijn voor Java-ontwikkelaars, omdat het een van de meest voorkomende runtime-fouten is die we kunnen tegenkomen.

In dit artikel zullen we zien hoe deze fout kan optreden door naar verschillende codevoorbeelden te kijken en hoe we ermee kunnen omgaan.

2. Frames stapelen en hoe StackOverflowError Komt voor

Laten we beginnen met de basis. Wanneer een methode wordt aangeroepen, wordt een nieuw stapelframe gemaakt op de aanroepstapel. Dit stapelframe bevat parameters van de aangeroepen methode, zijn lokale variabelen en het retouradres van de methode, d.w.z. het punt waarvandaan de uitvoering van de methode moet worden voortgezet nadat de aangeroepen methode is teruggekeerd.

Het maken van stapelframes gaat door totdat het einde van de methode-aanroepen die in geneste methoden worden gevonden, is bereikt.

Als JVM tijdens dit proces een situatie tegenkomt waarin er geen ruimte is om een ​​nieuw stapelframe te maken, zal het een StackOverflowError.

De meest voorkomende oorzaak voor de JVM om deze situatie tegen te komen is niet-beëindigde / oneindige recursie - de Javadoc-beschrijving voor StackOverflowError vermeldt dat de fout wordt gegenereerd als gevolg van een te diepe recursie in een bepaald codefragment.

Recursie is echter niet de enige oorzaak van deze fout. Het kan ook gebeuren in een situatie waarin een applicatie behouden blijft methoden aanroepen vanuit methoden totdat de stapel is uitgeput. Dit is een zeldzaam geval omdat geen enkele ontwikkelaar opzettelijk slechte coderingspraktijken zou volgen. Een andere zeldzame oorzaak is met een groot aantal lokale variabelen binnen een methode.

De StackOverflowError kan ook worden gegooid wanneer een applicatie is ontworpen om cyclische relaties tussen klassen. In deze situatie worden de constructeurs van elkaar herhaaldelijk aangeroepen, waardoor deze fout wordt gegenereerd. Dit kan ook worden beschouwd als een vorm van recursie.

Een ander interessant scenario dat deze fout veroorzaakt, is als een klasse wordt geïnstantieerd binnen dezelfde klasse als een instantievariabele van die klasse. Hierdoor wordt de constructor van dezelfde klasse keer op keer aangeroepen (recursief), wat uiteindelijk resulteert in een StackOverflowError.

In de volgende sectie zullen we enkele codevoorbeelden bekijken die deze scenario's demonstreren.

3. StackOverflowError in actie

In het onderstaande voorbeeld is een StackOverflowError zal worden gegenereerd als gevolg van onbedoelde recursie, waarbij de ontwikkelaar is vergeten een beëindigingsvoorwaarde op te geven voor het recursieve gedrag:

openbare klasse UnintendedInfiniteRecursion {openbare int berekenFactorial (int getal) {retournummer * berekenFactorial (getal - 1); }}

Hier wordt de fout bij alle gelegenheden gegenereerd voor elke waarde die aan de methode wordt doorgegeven:

openbare klasse UnintendedInfiniteRecursionManualTest {@Test (verwacht = StackOverflowError.class) openbare leegte gegevenPositiveIntNoOne_whenCalFact_thenThrowsException () {int numToCalcFactorial = 1; UnintendedInfiniteRecursion uir = nieuwe UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (verwacht = StackOverflowError.class) openbare leegte gegevenPositiveIntGtOne_whenCalcFact_thenThrowsException () {int numToCalcFactorial = 2; UnintendedInfiniteRecursion uir = nieuwe UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); } @Test (verwacht = StackOverflowError.class) openbare ongeldige gegevenNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; UnintendedInfiniteRecursion uir = nieuwe UnintendedInfiniteRecursion (); uir.calculateFactorial (numToCalcFactorial); }}

In het volgende voorbeeld wordt echter een beëindigingsvoorwaarde gespecificeerd, maar er wordt nooit aan voldaan als de waarde -1 wordt doorgegeven aan de berekenenFactorial () methode, die niet-beëindigde / oneindige recursie veroorzaakt:

openbare klasse InfiniteRecursionWithTerminationCondition {openbare int berekenFactorial (int getal) {retournummer == 1? 1: getal * berekenFactorial (getal - 1); }}

Deze reeks tests demonstreert dit scenario:

openbare klasse InfiniteRecursionWithTerminationConditionManualTest {@Test openbare leegte gegevenPositiveIntNoOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 1; InfiniteRecursionWithTerminationCondition irtc = nieuwe InfiniteRecursionWithTerminationCondition (); assertEquals (1, irtc.calculateFactorial (numToCalcFactorial)); } @Test openbare ongeldig gegevenPositiveIntGtOne_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = 5; InfiniteRecursionWithTerminationCondition irtc = nieuwe InfiniteRecursionWithTerminationCondition (); assertEquals (120, irtc.calculateFactorial (numToCalcFactorial)); } @Test (verwacht = StackOverflowError.class) openbare leegte gegevenNegativeInt_whenCalcFact_thenThrowsException () {int numToCalcFactorial = -1; InfiniteRecursionWithTerminationCondition irtc = nieuwe InfiniteRecursionWithTerminationCondition (); irtc.calculateFactorial (numToCalcFactorial); }}

In dit specifieke geval had de fout volledig voorkomen kunnen worden als de beëindigingsvoorwaarde simpelweg was gesteld als:

openbare klasse RecursionWithCorrectTerminationCondition {openbare int berekenFactorial (int getal) {retournummer <= 1? 1: getal * berekenFactorial (getal - 1); }}

Hier is de test die dit scenario in de praktijk laat zien:

openbare klasse RecursionWithCorrectTerminationConditionManualTest {@Test openbare leegte gegevenNegativeInt_whenCalcFact_thenCorrectlyCalc () {int numToCalcFactorial = -1; RecursionWithCorrectTerminationCondition rctc = nieuwe RecursionWithCorrectTerminationCondition (); assertEquals (1, rctc.calculateFactorial (numToCalcFactorial)); }}

Laten we nu eens kijken naar een scenario waarin de StackOverflowError gebeurt als gevolg van cyclische relaties tussen klassen. Laat ons nadenken ClassOne en Klasse Twee, die elkaar instantiëren binnen hun constructors waardoor een cyclische relatie ontstaat:

openbare klasse ClassOne {private int oneValue; private ClassTwo clsTwoInstance = null; openbare ClassOne () {oneValue = 0; clsTwoInstance = nieuwe ClassTwo (); } openbare ClassOne (int oneValue, ClassTwo clsTwoInstance) {this.oneValue = oneValue; this.clsTwoInstance = clsTwoInstance; }}
openbare klasse ClassTwo {private int twoValue; privé ClassOne clsOneInstance = null; openbare ClassTwo () {twoValue = 10; clsOneInstance = nieuwe ClassOne (); } openbare ClassTwo (int twoValue, ClassOne clsOneInstance) {this.twoValue = twoValue; this.clsOneInstance = clsOneInstance; }}

Laten we nu zeggen dat we proberen te instantiëren ClassOne zoals te zien in deze test:

openbare klasse CyclicDependancyManualTest {@Test (verwacht = StackOverflowError.class) openbare leegte whenInstanciatingClassOne_thenThrowsException () {ClassOne obj = nieuwe ClassOne (); }}

Dit eindigt met een StackOverflowError sinds de constructeur van ClassOne instantiëert Klasse Twee, en de constructeur van Klasse Twee opnieuw instantiëren ClassOne. En dit gebeurt herhaaldelijk totdat het de stapel overstroomt.

Vervolgens zullen we kijken wat er gebeurt wanneer een klasse wordt geïnstantieerd binnen dezelfde klasse als een instantievariabele van die klasse.

Zoals te zien is in het volgende voorbeeld, Rekeninghouder instantieert zichzelf als een instantievariabele jointAccountHolder:

openbare klasse AccountHolder {private String firstName; private String achternaam; AccountHolder jointAccountHolder = nieuwe AccountHolder (); }

Wanneer de Rekeninghouder klasse wordt geïnstantieerd, een StackOverflowError wordt gegenereerd vanwege de recursieve aanroep van de constructor zoals te zien in deze test:

openbare klasse AccountHolderManualTest {@Test (verwacht = StackOverflowError.class) openbare ongeldigheid whenInstanciatingAccountHolder_thenThrowsException () {AccountHolder-houder = nieuwe AccountHolder (); }}

4. Omgaan met StackOverflowError

Het beste wat u kunt doen als een StackOverflowError wordt aangetroffen, is om het stapeltracé voorzichtig te inspecteren om het zich herhalende patroon van regelnummers te identificeren. Dit zal ons in staat stellen om de code te lokaliseren met problematische recursie.

Laten we eens kijken naar een paar stacktraces die zijn veroorzaakt door de codevoorbeelden die we eerder zagen.

Deze stacktracering wordt geproduceerd door InfiniteRecursionWithTerminationConditionManualTest als we de verwacht uitzonderingsverklaring:

java.lang.StackOverflowError bij cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) bij cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) bij cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java:5) bij cbsInfiniteRecursionWithTerminationCondition .calculateFactorial (InfiniteRecursionWithTerminationCondition.java : 5)

Hier is regel nummer 5 te zien herhalen. Dit is waar de recursieve oproep wordt gedaan. Nu is het gewoon een kwestie van de code onderzoeken om te zien of de recursie op de juiste manier wordt uitgevoerd.

Hier is de stacktracering die we krijgen door uit te voeren Cyclische afhankelijkheid Handmatige test (nogmaals, zonder verwacht uitzondering):

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

Deze stacktracering toont de regelnummers die het probleem veroorzaken in de twee klassen die een cyclische relatie hebben. Regel nummer 9 van Klasse Twee en regel nummer 9 van de ClassOne wijs naar de locatie in de constructor waar het de andere klasse probeert te instantiëren.

Zodra de code grondig is geïnspecteerd en geen van de volgende zaken (of een andere codelogische fout) de oorzaak van de fout is:

  • Onjuist geïmplementeerde recursie (d.w.z. zonder beëindigingsvoorwaarde)
  • Cyclische afhankelijkheid tussen klassen
  • Instantiëren van een klasse binnen dezelfde klasse als een instantievariabele van die klasse

Het zou een goed idee zijn om te proberen de stapelgrootte te vergroten. Afhankelijk van de geïnstalleerde JVM, kan de standaardstapelgrootte variëren.

De -Xss flag kan worden gebruikt om de grootte van de stapel te vergroten, hetzij vanuit de configuratie van het project of via de opdrachtregel.

5. Conclusie

In dit artikel hebben we de StackOverflowError inclusief hoe Java-code het kan veroorzaken en hoe we het kunnen diagnosticeren en repareren.

Broncode met betrekking tot dit artikel is te vinden op GitHub.