Pourquoi cette méthode imprime 4?

Je me demandais ce qui se passe lorsque vous essayez d’attraper une StackOverflowError et que vous avez obtenu la méthode suivante:

class RandomNumberGenerator { static int cnt = 0; public static void main(Ssortingng[] args) { try { main(args); } catch (StackOverflowError ignore) { System.out.println(cnt++); } } } 

Maintenant ma question:

Pourquoi cette méthode imprime ‘4’?

J’ai pensé que c’était peut-être parce que System.out.println() besoin de 3 segments sur la stack 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 à bien plus d’invocations de méthodes que 3 (3 segments sur la stack d’appels ne seraient donc pas suffisants). Si c’est à cause des optimisations que la VM Hotspot applique (méthode d’inlining), je me demande si le résultat serait différent sur une autre VM.

Modifier :

Comme la sortie semble être spécifique à la JVM, j’obtiens le résultat 4 en utilisant
Java (TM) SE Runtime Environment (version 1.6.0_41-b02)
VM serveur Java HotSpot (64 bits) (version 20.14-b01, mode mixte)

Explication pourquoi je pense que cette question est différente de la compréhension de la stack Java :

Ma question n’est pas de savoir pourquoi il y a un cnt> 0 (évidemment parce que System.out.println() nécessite une taille de stack et lance une autre StackOverflowError avant que quelque chose ne soit imprimé), mais pourquoi il a la valeur 4, respectivement 0,3, 8,55 ou autre chose sur d’autres systèmes.

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 tellement entre les différents parameters. Je vais essayer de combler ce vide ici.

Laisser

  • X soit la taille totale de la stack
  • M être l’espace de stack utilisé lorsque nous entrons dans la main la première fois
  • R être l’espace de la stack augmenter chaque fois que nous entrons dans le principal
  • P soit l’espace de stack nécessaire pour exécuter System.out.println

Lorsque nous entrons dans le principal, l’espace restant est XM. Chaque appel récursif occupe plus de mémoire. Donc, pour 1 appel récursif (1 de plus que l’original), la mémoire utilisée est M + R. Supposons que StackOverflowError soit lancé 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 rest la mémoire X-M-C * R.

Pour pouvoir exécuter System.out.prinln , nous avons besoin de la quantité d’espace restant sur la stack. S’il arrive que X – M – C * R> = P, alors 0 sera imprimé. Si P requirejs plus d’espace, alors nous supprimons les frames de la stack, gagnant de la mémoire R au prix de cnt ++.

Lorsque println est enfin capable de s’exécuter, 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: Suppose

  • X = 100
  • M = 1
  • R = 2
  • P = 1

Alors C = sol ((XM) / R) = 49 et cnt = plafond ((P – (X – M – C * R)) / R) = 0.

Exemple 2: Supposons que

  • X = 100
  • M = 1
  • R = 5
  • P = 12

Alors C = 19 et cnt = 2.

Exemple 3: supposons que

  • X = 101
  • M = 1
  • R = 5
  • P = 12

Alors C = 20 et cnt = 3.

Exemple 4: Supposons que

  • X = 101
  • M = 2
  • R = 5
  • P = 12

Alors C = 19 et cnt = 2.

Ainsi, nous voyons que le système (M, R et P) et la taille de la stack (X) affectent cnt.

En guise de note, peu importe la quantité d’espace nécessaire pour démarrer. Tant qu’il n’y a pas assez d’espace pour la catch , alors cnt n’augmentera pas, donc il n’y a pas d’effets externes.

MODIFIER

Je reprends ce que j’ai dit à propos de la catch . Il joue un rôle. Supposons que vous ayez besoin d’espace T pour commencer. cnt commence à s’incrémenter lorsque l’espace restant est supérieur à T, et println s’exécute lorsque l’espace restant est supérieur à T + P. Cela ajoute une étape supplémentaire aux calculs et complique davantage l’parsing déjà boueuse.

MODIFIER

J’ai finalement trouvé le temps de faire des expériences pour sauvegarder 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 à 460 000.

Les résultats sont disponibles à l’ adresse suivante : https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM J’ai créé une autre version dans laquelle chaque sharepoint 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

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 stack dépend de la plate-forme. Java SE 6 sous Windows a une taille de stack par défaut de 320k dans la machine virtuelle 32 bits et 1024k dans la machine virtuelle 64 bits. Vous pouvez en lire plus ici .

Vous pouvez utiliser différentes tailles de stack et vous verrez différentes valeurs de cnt avant que la stack ne déborde.

java -Xss1024k RandomNumberGenerator

Vous ne voyez pas la valeur de cnt être imprimée plusieurs fois, même si la valeur est parfois supérieure à 1, car votre instruction print génère également une erreur que vous pouvez déboguer pour vous en assurer via Eclipse ou d’autres IDE.

Vous pouvez modifier le code pour le suivant pour déboguer par exécution de l’instruction si vous préférez

 static int cnt = 0; public static void main(Ssortingng[] args) { try { main(args); } catch (Throwable ignore) { cnt++; try { System.out.println(cnt); } catch (Throwable t) { } } } 

METTRE À JOUR:

Comme cela attire beaucoup plus d’attention, prenons un autre exemple pour rendre les choses plus claires.

 static int cnt = 0; public static void overflow(){ try { overflow(); } catch (Throwable t) { cnt++; } } public static void main(Ssortingng[] args) { overflow(); System.out.println(cnt); } 

Nous avons créé une autre méthode appelée overflow pour effectuer une récursivité incorrecte et avons supprimé l’instruction println du bloc catch afin qu’elle ne lance pas un autre jeu 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 comstackr. Ensuite, exécutez plusieurs fois. Selon votre plate-forme, vous pouvez obtenir différentes valeurs de cnt .

C’est pourquoi nous n’attrapons généralement pas d’erreurs car le mystère du code n’est pas un fantasme.

Le comportement dépend de la taille de la stack (qui peut être définie manuellement à l’aide de Xss . La taille de la stack est spécifique à l’architecture. A partir du code source JDK 7:

// La taille de stack par défaut sous Windows est déterminée par l’exécutable (java.exe
// a une valeur par défaut de 320K / 1MB [32bit / 64bit]). Selon la version de Windows, changer
// ThreadStackSize à non-zéro peut avoir un impact significatif sur l’utilisation de la mémoire.
// Voir les commentaires dans os_windows.cpp.

Donc, lorsque le StackOverflowError est lancé, l’erreur est interceptée dans le bloc catch. Ici, println() est un autre appel de stack qui génère à 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 de stackoverflow. Et cela dépend de la taille de stack de chaque appel de fonction (difficile à trouver) et du Xss . Comme mentionné ci-dessus, la taille et la taille totales par défaut de chaque appel de fonction (en fonction de la taille de la page de mémoire, etc.) sont spécifiques à la plate-forme. D’où des comportements différents.

L’appel de l’appel java avec -Xss 4M me donne 41 . D’où la corrélation.

Je pense que le nombre affiché est le nombre de fois que l’appel System.out.println renvoie l’exception Stackoverflow .

Cela dépend probablement de l’implémentation de println et du nombre d’appels empilés qu’il contient.

Pour illustrer:

L’appel main() déclenche l’exception Stackoverflow à l’appel i. L’appel i-1 de la prise principale intercepte l’exception et appelle println qui déclenche un deuxième Stackoverflow . cnt obtenir l’incrément à 1. L’appel i-2 de la prise principale maintenant l’exception et appeler println . Dans println une méthode est appelée déclencher une 3ème exception. cnt obtenir l’incrément à 2. ceci continue jusqu’à ce println puisse faire tout son appel nécessaire et finalement afficher la valeur de cnt .

Cela dépend alors de l’implémentation réelle de println .

Pour le JDK7, il détecte les appels cycliques et déclenche l’exception plus tôt, soit conserve certaines ressources de la stack et lance l’exception avant d’atteindre la limite pour laisser de la place à la logique de correction, soit l’implémentation de println après l’appel println est donc passé par l’exception.

  1. main récursive sur elle-même jusqu’à ce qu’elle déborde la stack à la profondeur de récursion R
  2. Le bloc catch à la profondeur de récursivité R-1 est exécuté.
  3. Le bloc catch à la profondeur de récursion R-1 évalue cnt++ .
  4. Le bloc catch en profondeur R-1 appelle println , plaçant l’ancienne valeur de cnt sur la stack. println appellera en interne d’autres méthodes et utilisera des variables et des objects locaux. Tous ces processus nécessitent un espace de stack.
  5. Comme la stack était déjà en train de brouter la limite et que l’appel / l’exécution de println nécessitait un espace de stack, un nouveau débordement de stack est déclenché à la profondeur R-1 au lieu de la profondeur R
  6. Les étapes 2 à 5 se reproduisent, mais à la profondeur de récursion R-2 .
  7. Les étapes 2 à 5 se reproduisent, mais à la profondeur de récursion R-3 .
  8. Les étapes 2 à 5 se reproduisent, mais à la profondeur de récursion R-4 .
  9. Les étapes 2 à 4 se reproduisent, mais à la profondeur de récursion R-5 .
  10. Il se trouve qu’il y a maintenant suffisamment d’espace de stack pour que println se termine (notez qu’il s’agit d’un détail d’implémentation, il peut varier).
  11. cnt été post-incrémenté aux profondeurs R-1 , R-2 , R-3 , R-4 et enfin à R-5 . Le cinquième post-incrémentation en a rapporté quatre, ce qui est ce qui a été imprimé.
  12. Avec main terminée avec succès à la profondeur R-5 , la stack entière se déroule sans autres blocs catch en cours d’exécution et le programme se termine.

Après avoir creusé pendant un moment, je ne peux pas dire que je trouve la réponse, mais je pense que c’est assez proche maintenant.

Tout d’abord, nous devons savoir quand une StackOverflowError sera lancée. En fait, la stack d’un thread Java stocke les frameworks, qui contiennent toutes les données nécessaires pour invoquer une méthode et la reprendre. Selon les spécifications de langage Java pour JAVA 6 , lorsque vous appelez une méthode,

S’il n’y a pas suffisamment de mémoire disponible pour créer un tel cadre d’activation, une erreur StackOverflowError est générée.

Deuxièmement, nous devrions clarifier ce qui est ” il n’y a pas suffisamment de mémoire disponible pour créer un tel cadre d’activation “. Selon les spécifications de la machine virtuelle Java pour JAVA 6 ,

les frames peuvent être des tas alloués.

Ainsi, lorsqu’une image est créée, il doit y avoir suffisamment d’espace de tas pour créer un cadre de stack et un espace suffisant pour stocker la nouvelle référence qui pointe vers le nouveau cadre de stack si le cadre est alloué au segment de mémoire.

Revenons maintenant à la question. De ce qui précède, nous pouvons savoir que, lorsqu’une méthode est exécutée, cela peut simplement coûter la même quantité d’espace de stack. Et l’invocation de System.out.println (may) nécessite 5 niveaux d’invocation de la méthode, de sorte que 5 frames doivent être créées. Ensuite, lorsque StackOverflowError est lancé, il doit revenir en arrière 5 fois pour obtenir suffisamment d’espace pour stocker 5 références de frameworks. Par conséquent, 4 est imprimé. Pourquoi pas 5? Parce que vous utilisez cnt++ . Changez-le en ++cnt , et vous obtiendrez 5.

Et vous remarquerez que lorsque la taille de la stack atteint un niveau élevé, vous en aurez parfois 50. C’est parce que la quantité d’espace mémoire disponible doit être prise en compte. Lorsque la taille de la stack est trop importante, l’espace mémoire risque de manquer avant la stack. Et (peut-être) la taille réelle des frames de stack de System.out.println est d’environ 51 fois celle de main , donc 51 fois et 50 sont imprimées.

Ce n’est pas exactement une réponse à la question, mais je voulais juste append quelque chose à la question originale que j’ai trouvée et comment j’ai compris le problème:

Dans le problème d’origine, l’exception est détectée là où c’était possible:

Par exemple, avec jdk 1.7, il est pris en premier lieu.

mais dans les versions antérieures de jdk, il semblerait que l’exception ne soit pas interceptée au premier endroit, par exemple 4, 50, etc.

Maintenant, si vous supprimez le bloc try catch comme suit

 public static void main( Ssortingng[] args ){ System.out.println(cnt++); main(args); } 

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 ne montrera pas toutes les sorties et les exceptions levées.