Qu'est-ce qu'un StackOverflowError
, quelle en est la cause et comment dois-je les gérer?
Les paramètres et les variables locales sont alloués sur le pile (avec les types de référence, l'objet vit sur le tas et une variable de la pile référence cet objet sur le tas). La pile habite généralement à la fin de votre espace d’adresse en haut et, au fur et à mesure de son utilisation, elle se dirige vers la en bas de l’espace de mémoire (c’est-à-dire vers zéro).
Votre processus a également un tas, qui vit à la fin du bas de votre processus. Lorsque vous allouez de la mémoire, ce segment de mémoire peut atteindre la limite supérieure de votre espace d'adressage. Comme vous pouvez le constater, il y a un potentiel pour que le tas "collide" avec la pile (un peu comme des plaques tectoniques !!!).
La cause commune d'un débordement de pile est un mauvais appel récursif. Cela est généralement dû au fait que vos fonctions récursives ne présentent pas la condition de terminaison correcte; elles finissent donc par s'appeler elles-mêmes pour toujours. Ou bien, lorsque la condition de terminaison est correcte, cela peut être dû à un trop grand nombre d'appels récursifs avant de la remplir.
Cependant, avec la programmation graphique, il est possible de générer récursion indirecte. Par exemple, votre application peut gérer les messages Paint et, lors de leur traitement, appeler une fonction qui entraîne l'envoi par le système d'un autre message Paint. Ici, vous ne vous êtes pas explicitement appelé, mais l’OS/VM l’a fait pour vous.
Pour les traiter, vous devrez examiner votre code. Si vous avez des fonctions qui s'appellent elles-mêmes, vérifiez que vous avez une condition de fin. Si tel est le cas, vérifiez que lorsque vous appelez la fonction, vous avez au moins modifié l'un des arguments, sinon il n'y aura aucun changement visible pour la fonction appelée récursivement et la condition de terminaison est inutile. N'oubliez pas non plus que votre espace de pile peut manquer de mémoire avant d'atteindre une condition de fin valide. Assurez-vous donc que votre méthode peut gérer les valeurs d'entrée nécessitant des appels plus récursifs.
Si vous n'avez aucune fonction récursive évidente, vérifiez si vous appelez des fonctions de bibliothèque qui indirectement provoqueront l'appel de votre fonction (comme dans le cas implicite ci-dessus).
Pour décrire cela, commençons par comprendre comment les variables et les objets local sont stockés.
Les variables locales sont stockées dans pile:
Si vous regardez l'image, vous devriez être capable de comprendre comment les choses fonctionnent.
Lorsqu'un appel de fonction est appelé par une application Java, un cadre de pile est alloué sur la pile d'appels. Le cadre de pile contient les paramètres de la méthode invoquée, ses paramètres locaux et l'adresse de retour de la méthode. L'adresse de retour indique le point d'exécution à partir duquel l'exécution du programme doit continuer après le retour de la méthode invoquée. S'il n'y a pas d'espace pour un nouveau cadre de pile, la StackOverflowError
est renvoyée par la Java machine virtuelle (JVM).
Le cas le plus courant pouvant éventuellement épuiser la pile d’une application Java est la récursivité. En récursion, une méthode s’appelle elle-même lors de son exécution. La récursivité est considérée comme une puissante technique de programmation polyvalente, mais doit être utilisée avec prudence afin d'éviter StackOverflowError
.
Un exemple jetant un StackOverflowError
est montré ci-dessous:
StackOverflowErrorExample.Java:
public class StackOverflowErrorExample {
public static void recursivePrint(int num) {
System.out.println("Number: " + num);
if(num == 0)
return;
else
recursivePrint(++num);
}
public static void main(String[] args) {
StackOverflowErrorExample.recursivePrint(1);
}
}
Dans cet exemple, nous définissons une méthode récursive, appelée recursivePrint
, qui affiche un entier, puis s’appelle elle-même, l’entier suivant étant l’argument. La récursivité se termine jusqu'à ce que nous passions _ en tant que paramètre dans 0
. Cependant, dans notre exemple, nous avons passé le paramètre de 1 et son nombre croissant de suiveurs. Par conséquent, la récursivité ne se terminera jamais.
Un exemple d'exécution, utilisant l'indicateur -Xss1M
spécifiant que la taille de la pile de threads doit être égale à 1 Mo, est présenté ci-dessous:
Number: 1
Number: 2
Number: 3
...
Number: 6262
Number: 6263
Number: 6264
Number: 6265
Number: 6266
Exception in thread "main" Java.lang.StackOverflowError
at Java.io.PrintStream.write(PrintStream.Java:480)
at Sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.Java:221)
at Sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.Java:291)
at Sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.Java:104)
at Java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.Java:185)
at Java.io.PrintStream.write(PrintStream.Java:527)
at Java.io.PrintStream.print(PrintStream.Java:669)
at Java.io.PrintStream.println(PrintStream.Java:806)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.Java:4)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.Java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.Java:9)
at StackOverflowErrorExample.recursivePrint(StackOverflowErrorExample.Java:9)
...
En fonction de la configuration initiale de la machine virtuelle, les résultats peuvent différer, mais le fichier StackOverflowError
doit éventuellement être lancé. Cet exemple est un très bon exemple de la façon dont la récursivité peut causer des problèmes, si elle n'est pas implémentée avec prudence.
Comment traiter avec StackOverflowError
La solution la plus simple consiste à examiner soigneusement le tracé de la pile et à détecter le motif de répétition des numéros de ligne. Ces numéros de ligne indiquent le code appelé de manière récursive. Une fois que vous avez détecté ces lignes, vous devez soigneusement inspecter votre code et comprendre pourquoi la récursion ne se termine jamais.
Si vous avez vérifié que la récursivité est correctement implémentée, vous pouvez augmenter la taille de la pile afin de permettre un plus grand nombre d’appels. Selon la machine virtuelle (JVM) Java installée, la taille de pile de threads par défaut peut être égale à 512 Ko ou 1 Mo. Vous pouvez augmenter la taille de la pile de threads à l'aide de l'indicateur -Xss
. Cet indicateur peut être spécifié via la configuration du projet ou via la ligne de commande. Le format de l'argument -Xss
est le suivant: -Xss<size>[g|G|m|M|k|K]
Si vous avez une fonction comme:
int foo()
{
// more stuff
foo();
}
Ensuite, foo () continuera à s’appeler lui-même, devenant de plus en plus profond, et lorsque l’espace utilisé pour garder trace des fonctions dans lesquelles vous êtes est rempli, vous obtenez l’erreur de débordement de pile.
Dépassement de pile signifie exactement cela: une pile déborde. Il y a généralement une pile dans le programme qui contient les variables de portée locale et les adresses où retourner lorsque l'exécution d'une routine se termine. Cette pile a tendance à être une plage de mémoire fixe quelque part dans la mémoire. Par conséquent, son contenu peut être limité.
Si la pile est vide, vous ne pouvez pas ouvrir, sinon vous obtiendrez une erreur de débordement de pile.
Si la pile est pleine, vous ne pouvez pas pousser, sinon vous obtiendrez une erreur de débordement de pile.
Donc, le dépassement de pile apparaît là où vous allouez trop dans la pile. Par exemple, dans la récursion mentionnée.
Certaines implémentations optimisent certaines formes de récursivité. Queue récursive en particulier. Les routines récursives de la queue sont des formes de routines dans lesquelles l'appel récursif apparaît comme la dernière chose que fait la routine. Un tel appel de routine se réduit simplement à un saut.
Certaines implémentations vont jusqu'à implémenter leurs propres piles pour la récursivité. Elles permettent donc à la récursivité de continuer jusqu'à ce que le système manque de mémoire.
La meilleure chose à faire serait d’augmenter la taille de votre pile si vous le pouvez. Si vous ne pouvez pas faire cela, la deuxième meilleure chose à faire serait de vérifier si quelque chose cause clairement le débordement de pile. Essayez-le en imprimant quelque chose avant et après l'appel dans la routine. Cela vous aide à découvrir la routine défaillante.
Un débordement de pile est généralement appelé par des appels de fonction d'imbrication trop profonds (particulièrement facile lorsque vous utilisez la récursivité, c'est-à-dire une fonction qui s'appelle elle-même) ou que vous allouez une grande quantité de mémoire sur la pile où l'utilisation du segment de mémoire serait plus appropriée.
Comme vous le dites, vous devez montrer du code. :-)
Une erreur de débordement de pile se produit généralement lorsque vos appels de fonction imbriquent trop profondément. Voir le fil Stack Overflow Code Golf pour quelques exemples. (Dans le cas de cette question, les réponses provoquent intentionnellement un débordement de pile).
StackOverflowError
est à la pile comme OutOfMemoryError
est à la pile.
Les appels récursifs non liés entraînent une utilisation de l'espace de pile.
L'exemple suivant produit StackOverflowError
:
class StackOverflowDemo
{
public static void unboundedRecursiveCall() {
unboundedRecursiveCall();
}
public static void main(String[] args)
{
unboundedRecursiveCall();
}
}
StackOverflowError
est évitable si les appels récursifs sont liés pour empêcher le total cumulé des appels en mémoire incomplets (en octets) de dépasser la taille de la pile (en octets).
La cause la plus fréquente de débordements de pile est une récursion excessivement profonde ou infinie . Si tel est votre problème, ce tutoriel sur Java Recursion pourrait vous aider à comprendre le problème.
Un StackOverflowError
est une erreur d'exécution en Java.
Il est émis lorsque la quantité de mémoire de la pile d'appels allouée par la machine virtuelle Java est dépassée.
Un cas courant de StackOverflowError
en cours de projection survient lorsque la pile d'appels dépasse en raison d'une récursion excessive ou infinie excessive.
Exemple:
public class Factorial {
public static int factorial(int n){
if(n == 1){
return 1;
}
else{
return n * factorial(n-1);
}
}
public static void main(String[] args){
System.out.println("Main method started");
int result = Factorial.factorial(-1);
System.out.println("Factorial ==>"+result);
System.out.println("Main method ended");
}
}
Trace de la pile:
Main method started
Exception in thread "main" Java.lang.StackOverflowError
at com.program.stackoverflow.Factorial.factorial(Factorial.Java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.Java:9)
at com.program.stackoverflow.Factorial.factorial(Factorial.Java:9)
Dans le cas ci-dessus, cela peut être évité en faisant des changements de programme. Mais si la logique du programme est correcte et qu'elle se produit toujours, la taille de votre pile doit être augmentée.
Voici un exemple d'algorithme récursif pour inverser une liste liée simplement. Sur un ordinateur portable répondant aux spécifications suivantes (mémoire 4G, processeur Intel Core i5 à 2,3 GHz, Windows 7 64 bits), cette fonction rencontrera une erreur StackOverflow pour une liste chaînée de taille proche de 10 000.
Ce que je veux dire, c'est que nous devrions utiliser judicieusement la récursivité en tenant toujours compte de la taille du système. La récursivité peut souvent être convertie en programme itératif, qui évolue mieux. (Une version itérative du même algorithme est donnée au bas de la page, elle inverse une liste simple de 1 million en 9 millisecondes.)
private static LinkedListNode doReverseRecursively(LinkedListNode x, LinkedListNode first){
LinkedListNode second = first.next;
first.next = x;
if(second != null){
return doReverseRecursively(first, second);
}else{
return first;
}
}
public static LinkedListNode reverseRecursively(LinkedListNode head){
return doReverseRecursively(null, head);
}
Version itérative du même algorithme:
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
private static LinkedListNode doReverseIteratively(LinkedListNode x, LinkedListNode first) {
while (first != null) {
LinkedListNode second = first.next;
first.next = x;
x = first;
if (second == null) {
break;
} else {
first = second;
}
}
return first;
}
public static LinkedListNode reverseIteratively(LinkedListNode head){
return doReverseIteratively(null, head);
}
Voici un exemple
public static void main(String[] args) {
System.out.println(add5(1));
}
public static int add5(int a) {
return add5(a) + 5;
}
En gros, une StackOverflowError se produit lorsque vous essayez de faire quelque chose, que le plus souvent s'appelle lui-même et continue à l'infini (ou jusqu'à ce qu'il donne une erreur StackOverflowError).
add5(a)
s'appellera, puis s'appellera à nouveau, etc.
Le terme "dépassement de pile (débordement)" est souvent utilisé, mais un abus de langage; les attaques ne débordent pas de la pile mais des tampons sur la pile.
- de diapositives de lecture de Prof. Dr. Dieter Gollmann
Il s'agit d'un cas typique de Java.lang.StackOverflowError
... La méthode s'appelle de manière récursive sans sortie dans doubleValue()
, floatValue()
, etc.
public class Rational extends Number implements Comparable<Rational> {
private int num;
private int denom;
public Rational(int num, int denom) {
this.num = num;
this.denom = denom;
}
public int compareTo(Rational r) {
if ((num / denom) - (r.num / r.denom) > 0) {
return +1;
} else if ((num / denom) - (r.num / r.denom) < 0) {
return -1;
}
return 0;
}
public Rational add(Rational r) {
return new Rational(num + r.num, denom + r.denom);
}
public Rational sub(Rational r) {
return new Rational(num - r.num, denom - r.denom);
}
public Rational mul(Rational r) {
return new Rational(num * r.num, denom * r.denom);
}
public Rational div(Rational r) {
return new Rational(num * r.denom, denom * r.num);
}
public int gcd(Rational r) {
int i = 1;
while (i != 0) {
i = denom % r.denom;
denom = r.denom;
r.denom = i;
}
return denom;
}
public String toString() {
String a = num + "/" + denom;
return a;
}
public double doubleValue() {
return (double) doubleValue();
}
public float floatValue() {
return (float) floatValue();
}
public int intValue() {
return (int) intValue();
}
public long longValue() {
return (long) longValue();
}
}
public class Main {
public static void main(String[] args) {
Rational a = new Rational(2, 4);
Rational b = new Rational(2, 6);
System.out.println(a + " + " + b + " = " + a.add(b));
System.out.println(a + " - " + b + " = " + a.sub(b));
System.out.println(a + " * " + b + " = " + a.mul(b));
System.out.println(a + " / " + b + " = " + a.div(b));
Rational[] arr = {new Rational(7, 1), new Rational(6, 1),
new Rational(5, 1), new Rational(4, 1),
new Rational(3, 1), new Rational(2, 1),
new Rational(1, 1), new Rational(1, 2),
new Rational(1, 3), new Rational(1, 4),
new Rational(1, 5), new Rational(1, 6),
new Rational(1, 7), new Rational(1, 8),
new Rational(1, 9), new Rational(0, 1)};
selectSort(arr);
for (int i = 0; i < arr.length - 1; ++i) {
if (arr[i].compareTo(arr[i + 1]) > 0) {
System.exit(1);
}
}
Number n = new Rational(3, 2);
System.out.println(n.doubleValue());
System.out.println(n.floatValue());
System.out.println(n.intValue());
System.out.println(n.longValue());
}
public static <T extends Comparable<? super T>> void selectSort(T[] array) {
T temp;
int mini;
for (int i = 0; i < array.length - 1; ++i) {
mini = i;
for (int j = i + 1; j < array.length; ++j) {
if (array[j].compareTo(array[mini]) < 0) {
mini = j;
}
}
if (i != mini) {
temp = array[i];
array[i] = array[mini];
array[mini] = temp;
}
}
}
}
2/4 + 2/6 = 4/10
Exception in thread "main" Java.lang.StackOverflowError
2/4 - 2/6 = 0/-2
at com.xetrasu.Rational.doubleValue(Rational.Java:64)
2/4 * 2/6 = 4/24
at com.xetrasu.Rational.doubleValue(Rational.Java:64)
2/4 / 2/6 = 12/8
at com.xetrasu.Rational.doubleValue(Rational.Java:64)
at com.xetrasu.Rational.doubleValue(Rational.Java:64)
at com.xetrasu.Rational.doubleValue(Rational.Java:64)
at com.xetrasu.Rational.doubleValue(Rational.Java:64)
at com.xetrasu.Rational.doubleValue(Rational.Java:64)