Je me demandais ce qui se passe lorsque vous essayez d'attraper une StackOverflowError et que vous avez proposé la méthode suivante:
class RandomNumberGenerator {
static int cnt = 0;
public static void main(String[] args) {
try {
main(args);
} catch (StackOverflowError ignore) {
System.out.println(cnt++);
}
}
}
Maintenant ma question:
Pourquoi cette méthode imprime-t-elle «4»?
Je pensais que c'était peut-être parce qu'il System.out.println()
fallait 3 segments sur la pile d'appels, mais je ne sais pas d'où vient le numéro 3. Lorsque vous regardez le code source (et le bytecode) de System.out.println()
, cela conduirait normalement à beaucoup plus d'appels de méthode que 3 (donc 3 segments sur la pile d'appels ne seraient pas suffisants). Si c'est à cause des optimisations que la VM Hotspot applique (méthode inlining), je me demande si le résultat serait différent sur une autre VM.
Modifier :
Comme la sortie semble être très spécifique à la JVM, j'obtiens le résultat 4 en utilisant
Java (TM) SE Runtime Environment (build 1.6.0_41-b02)
Java HotSpot (TM) 64-Bit Server VM (build 20.14-b01, mode mixte)
Explication pourquoi je pense que cette question est différente de Comprendre la pile Java :
Ma question n'est pas de savoir pourquoi il y a un cnt> 0 (évidemment parce qu'il System.out.println()
nécessite une taille de pile et en jette une autre StackOverflowError
avant que quelque chose ne soit imprimé), mais pourquoi il a la valeur particulière de 4, respectivement 0,3,8,55 ou autre chose systèmes.
la source
5
,6
et38
avec Java 1.7.0_10Réponses:
Je pense que les autres ont fait un bon travail pour expliquer pourquoi cnt> 0, mais il n'y a pas assez de détails sur pourquoi cnt = 4, et pourquoi cnt varie si largement entre les différents paramètres. Je vais essayer de combler ce vide ici.
Laisser
System.out.println
Lorsque nous entrons pour la première fois dans main, l'espace restant est XM. Chaque appel récursif occupe R plus de mémoire. Donc, pour 1 appel récursif (1 de plus que l'original), l'utilisation de la mémoire est M + R. Supposons que StackOverflowError soit levé après C appels récursifs réussis, c'est-à-dire M + C * R <= X et M + C * (R + 1)> X. Au moment du premier StackOverflowError, il reste de la mémoire X - M - C * R.
Pour pouvoir fonctionner
System.out.prinln
, nous avons besoin de P d'espace libre sur la pile. S'il arrive que X - M - C * R> = P, alors 0 sera imprimé. Si P nécessite plus d'espace, alors nous supprimons les cadres de la pile, gagnant de la mémoire R au prix de cnt ++.Quand
println
est enfin capable de fonctionner, X - M - (C - cnt) * R> = P. Donc si P est grand pour un système particulier, alors cnt sera grand.Regardons cela avec quelques exemples.
Exemple 1: Supposons
Alors C = sol ((XM) / R) = 49, et cnt = plafond ((P - (X - M - C * R)) / R) = 0.
Exemple 2: supposons que
Alors C = 19 et cnt = 2.
Exemple 3: Supposons que
Alors C = 20 et cnt = 3.
Exemple 4: Supposons que
Alors C = 19 et cnt = 2.
Ainsi, nous voyons que le système (M, R et P) et la taille de la pile (X) affectent cnt.
En remarque, peu importe l'espace
catch
requis pour démarrer. Tant qu'il n'y a pas assez de place pourcatch
, alors cnt n'augmentera pas, donc il n'y a pas d'effets externes.ÉDITER
Je reprends ce que j'ai dit
catch
. Cela joue un rôle. Supposons qu'il nécessite une quantité d'espace T pour démarrer. cnt commence à s'incrémenter lorsque l'espace restant est supérieur à T, etprintln
s'exécute lorsque l'espace restant est supérieur à T + P. Cela ajoute une étape supplémentaire aux calculs et brouille encore l'analyse déjà trouble.ÉDITER
J'ai finalement trouvé le temps de mener des expériences pour étayer ma théorie. Malheureusement, la théorie ne semble pas correspondre aux expériences. Ce qui se passe réellement est très différent.
Configuration de l'expérience: serveur Ubuntu 12.04 avec java par défaut et default-jdk. Xss commençant à 70 000 par incréments de 1 octet jusqu'à 460 000.
Les résultats sont disponibles à l' adresse : https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM J'ai créé une autre version dans laquelle chaque point de données répété est supprimé. En d'autres termes, seuls les points différents des précédents sont affichés. Cela facilite la détection des anomalies. https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA
la source
StackOverflowError
est lancée et comment cela affecte la sortie. S'il ne contenait qu'une référence à un cadre de pile sur le tas (comme Jay l'a suggéré), alors la sortie devrait être tout à fait prévisible pour un système donné.C'est la victime d'un mauvais appel récursif. Comme vous vous demandez pourquoi la valeur de cnt varie, c'est parce que la taille de la pile dépend de la plate-forme. Java SE 6 sur Windows a une taille de pile par défaut de 320 Ko dans la machine virtuelle 32 bits et de 1024 Ko dans la machine virtuelle 64 bits. Vous pouvez en savoir plus ici .
Vous pouvez exécuter en utilisant différentes tailles de pile et vous verrez différentes valeurs de cnt avant que la pile ne déborde.
Vous ne voyez pas la valeur de cnt imprimée plusieurs fois même si la valeur est parfois supérieure à 1 car votre instruction d'impression génère également une erreur que vous pouvez déboguer pour être sûr via Eclipse ou d'autres IDE.
Vous pouvez changer le code comme suit pour déboguer par exécution d'instruction si vous préférez:
METTRE À JOUR:
Au fur et à mesure que cela suscite beaucoup plus d'attention, prenons un autre exemple pour clarifier les choses.
Nous avons créé une autre méthode nommée overflow pour effectuer une mauvaise récursivité et supprimé l' instruction println du bloc catch afin qu'elle ne commence pas à lancer un autre ensemble d'erreurs lors de la tentative d'impression. Cela fonctionne comme prévu. Vous pouvez essayer de mettre System.out.println (cnt); instruction après cnt ++ ci-dessus et compilez. Puis exécutez plusieurs fois. En fonction de votre plate-forme, vous pouvez obtenir différentes valeurs de cnt .
C'est pourquoi généralement nous ne détectons pas les erreurs car le mystère dans le code n'est pas de la fantaisie.
la source
Le comportement dépend de la taille de la pile (qui peut être définie manuellement à l'aide de
Xss
. La taille de la pile est spécifique à l'architecture. À partir du code source JDK 7 :Ainsi, lorsque le
StackOverflowError
est lancé, l'erreur est interceptée dans le bloc catch. Voiciprintln()
un autre appel de pile qui lève à nouveau une exception. Cela se répète.Combien de fois ça se répète? - Eh bien, cela dépend du moment où JVM pense qu'il ne s'agit plus d'un stackoverflow. Et cela dépend de la taille de la pile de chaque appel de fonction (difficile à trouver) et du fichier
Xss
. Comme mentionné ci-dessus, la taille totale et la taille par défaut de chaque appel de fonction (dépend de la taille de la page mémoire, etc.) sont spécifiques à la plate-forme. D'où un comportement différent.Appeler l'
java
appel avec-Xss 4M
me donne41
. D'où la corrélation.la source
catch
est un bloc et qui occupe la mémoire sur la pile. La quantité de mémoire que prend chaque appel de méthode ne peut pas être connue. Lorsque la pile est effacée, vous ajoutez un autre bloc decatch
et ainsi de suite. Cela pourrait être le comportement. Ce n'est que spéculation.Je pense que le nombre affiché est le nombre de fois où l'
System.out.println
appel lève l'Stackoverflow
exception.Cela dépend probablement de l'implémentation du
println
et du nombre d'appels d'empilement qu'il est effectué.Pour illustrer:
L'
main()
appel déclenche l'Stackoverflow
exception à l'appel i. L'appel i-1 de main capture l'exception et l'appelprintln
qui en déclenche une secondeStackoverflow
.cnt
obtenir un incrément de 1. L'appel i-2 de main catch maintenant l'exception et l'appelprintln
. Dansprintln
une méthode, on appelle déclencher une 3e exception.cnt
obtenir un incrément de 2. cela continue jusqu'à ce que vousprintln
puissiez effectuer tous les appels nécessaires et finalement afficher la valeur decnt
.Cela dépend alors de la mise en œuvre réelle de
println
.Pour le JDK7, soit il détecte l'appel cyclique et lève l'exception plus tôt, soit il conserve une ressource de pile et lance l'exception avant d'atteindre la limite pour donner de la place à la logique de correction soit l'
println
implémentation ne fait pas d'appels soit l'opération ++ est effectuée après l'println
appel est donc contourné par l'exception.la source
main
se répète sur lui-même jusqu'à ce qu'il déborde de la pile à la profondeur de récursivitéR
.R-1
est exécuté.R-1
évaluecnt++
.R-1
appelleprintln
, plaçantcnt
l'ancienne valeur de la pile.println
appellera en interne d'autres méthodes et utilisera des variables locales et des choses. Tous ces processus nécessitent un espace de pile.println
nécessite de l'espace dans la pile, un nouveau débordement de pile est déclenché en profondeurR-1
au lieu de profondeurR
.R-2
.R-3
.R-4
.R-5
.println
terminer (notez qu'il s'agit d'un détail d'implémentation, cela peut varier).cnt
a été post-incrémenté à des profondeursR-1
,R-2
,R-3
,R-4
et enfin àR-5
. Le cinquième post-incrément a renvoyé quatre, ce qui a été imprimé.main
terminé avec succès en profondeurR-5
, la pile entière se déroule sans que d'autres blocs catch soient exécutés et le programme se termine.la source
Après avoir fouillé pendant un moment, je ne peux pas dire que je trouve la réponse, mais je pense que c'est assez proche maintenant.
Premièrement, nous devons savoir quand un
StackOverflowError
testament sera lancé. En fait, la pile d'un thread java stocke des frames, qui contiennent toutes les données nécessaires pour appeler une méthode et reprendre. Selon les spécifications du langage Java pour JAVA 6 , lors de l'appel d'une méthode,Deuxièmement, nous devons préciser ce que signifie " il n'y a pas suffisamment de mémoire disponible pour créer une telle trame d'activation ". Selon les spécifications de la machine virtuelle Java pour JAVA 6 ,
Ainsi, quand un cadre est créé, il doit y avoir suffisamment d'espace de tas pour créer un cadre de pile et suffisamment d'espace de pile pour stocker la nouvelle référence qui pointe vers le nouveau cadre de pile si le cadre est alloué au tas.
Revenons maintenant à la question. D'après ce qui précède, nous pouvons savoir que lorsqu'une méthode est exécutée, elle peut simplement coûter la même quantité d'espace de pile. Et l'invocation
System.out.println
(peut) nécessite 5 niveaux d'invocation de méthode, donc 5 cadres doivent être créés. Ensuite, lorsqu'ilStackOverflowError
est jeté, il doit revenir 5 fois en arrière pour obtenir suffisamment d'espace de pile pour stocker les références de 5 cadres. Par conséquent, 4 est une impression. Pourquoi pas 5? Parce que vous utilisezcnt++
. Changez-le en++cnt
, et vous obtiendrez 5.Et vous remarquerez que lorsque la taille de la pile atteint un niveau élevé, vous en obtiendrez parfois 50. En effet, la quantité d'espace de tas disponible doit alors être prise en compte. Lorsque la taille de la pile est trop grande, il se peut que l'espace du tas soit épuisé avant la pile. Et (peut-être) la taille réelle des cadres de pile
System.out.println
est d'environ 51 foismain
, donc elle revient 51 fois et imprime 50.la source
System.out.print
ou la stratégie d'exécution de la méthode. Comme décrit ci-dessus, l'implémentation de la VM affecte également l'endroit où la trame de pile sera réellement stockée.Ce n'est pas exactement une réponse à la question, mais je voulais juste ajouter quelque chose à la question originale que j'ai rencontrée et comment j'ai compris le problème:
Dans le problème d'origine, l'exception est interceptée là où c'était possible:
Par exemple, avec jdk 1.7, il est capturé au premier lieu d'occurrence.
mais dans les versions antérieures de jdk, il semble que l'exception ne soit pas interceptée au premier lieu d'occurrence donc 4, 50 etc.
Maintenant, si vous supprimez le bloc try catch comme suit
Ensuite, vous verrez toutes les valeurs de
cnt
ant les exceptions levées (sur jdk 1.7).J'ai utilisé netbeans pour voir la sortie, car la cmd n'affichera pas toute la sortie et l'exception levée.
la source