web-dev-qa-db-fra.com

Pourquoi cette méthode imprime-t-elle 4?

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 que System.out.println() avait besoin de 3 segments sur la pile d'appels, mais je ne sais pas d'où vient le numéro 3. Quand vous regardez le code source (et 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 du Hotspot VM s'applique (méthode inlining), je me demande si le résultat serait différent sur une autre VM.

Éditer:

Comme la sortie semble être très spécifique à la JVM, j'obtiens le résultat 4 en utilisant
Environnement d'exécution Java (TM) SE (build 1.6.0_41-b02)
Serveur 64 bits Java HotSpot (TM) VM (build 20.14-b01, mode mixte)


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

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 pile et lance un 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 sur d'autres systèmes.

110
flrnb

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

  • X est la taille totale de la pile
  • M être l'espace de pile utilisé lorsque nous entrons dans main la première fois
  • R soit l'augmentation de l'espace de pile à chaque fois que nous entrons dans
  • P est l'espace de pile nécessaire pour exécuter System.out.println

Lorsque nous entrons pour la première fois dans main, l'espace restant est X-M. 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 exécuter System.out.prinln, nous avons besoin de P espace restant 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 trames de la pile, gagnant de la mémoire R au prix de cnt ++.

Quand 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: Supposons

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

Alors C = sol ((X-M)/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 pile (X) affectent cnt.

En remarque, peu importe l'espace dont catch a besoin pour démarrer. Tant qu'il n'y a pas assez d'espace pour 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 catch. Cela joue un rôle. Supposons que cela nécessite T espace pour démarrer. 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 encore l'analyse déjà trouble.

[~ # ~] modifier [~ # ~]

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 par défaut Java et default-jdk. Xss à partir de 70 000 par incréments de 1 octet à 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

41
John Tseng

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 le 32 bits VM et 1024 Ko dans la VM 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 -

Java -Xss1024k RandomNumberGenerator

Vous ne voyez pas la valeur de cnt imprimée plusieurs fois même si la valeur est parfois supérieure à 1 parce que votre instruction d'impression génère également une erreur que vous pouvez déboguer pour être sûr via Eclipse ou autre IDE.

Vous pouvez changer le code comme suit pour déboguer par exécution d'instruction si vous préférez:

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

MISE À JOUR:

Au fur et à mesure que cela suscite beaucoup plus d'attention, prenons un autre exemple pour clarifier les choses -

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

Nous avons créé une autre méthode nommée overflow pour faire une mauvaise récursivité et supprimé l'instruction println du bloc catch afin qu'elle ne commence pas à lancer un autre ensemble d'erreurs pendant essayant d'imprimer. Cela fonctionne comme prévu. Vous pouvez essayer de mettre System.out.println (cnt); instruction après cnt ++ ci-dessus et compiler. Puis exécutez plusieurs fois. Selon votre plate-forme, vous pouvez obtenir des valeurs différentes 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.

20
Sajal Dutta

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 JDK 7 code source :

// La taille de pile par défaut sous Windows est déterminée par l'exécutable (Java.exe
// a une valeur par défaut de 320 Ko/1 Mo [32 bits/64 bits]). Selon la version de Windows, changer
// ThreadStackSize non nul peut avoir un impact significatif sur l'utilisation de la mémoire.
// Voir les commentaires dans os_windows.cpp.

Ainsi, lorsque le StackOverflowError est lancé, l'erreur est interceptée dans le bloc catch. Ici println() est un autre appel de pile qui lève à nouveau une exception. Cela se répète.

Combien de fois il se répète? - Eh bien, cela dépend du moment où JVM pense que ce n'est plus un stackoverflow. Et cela dépend de la taille de la pile de chaque appel de fonction (difficile à trouver) et du 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.

Appel de l'appel Java avec -Xss 4M Donne moi 41. D'où la corrélation.

13
Jatin
  1. main se répète sur lui-même jusqu'à ce qu'il déborde la pile à la profondeur de récursivité R.
  2. Le bloc catch à la profondeur de récursivité R-1 est exécuté.
  3. Le bloc catch à la profondeur de récursivité R-1 évalue cnt++.
  4. Le bloc de capture en profondeur R-1 appelle println, plaçant l'ancienne valeur de cnt sur 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.
  5. Comme la pile frôlait déjà la limite et que l'appel/l'exécution de println nécessite de l'espace dans la pile, un nouveau débordement de pile est déclenché en profondeur R-1 au lieu de profondeur R.
  6. Les étapes 2 à 5 se reproduisent, mais à la profondeur de récursivité R-2.
  7. Les étapes 2 à 5 se reproduisent, mais à la profondeur de récursivité R-3.
  8. Les étapes 2 à 5 se reproduisent, mais à la profondeur de récursivité R-4.
  9. Les étapes 2 à 4 se reproduisent, mais à la profondeur de récursivité R-5.
  10. Il se trouve qu'il y a maintenant assez d'espace dans la pile pour que println se termine (notez qu'il s'agit d'un détail d'implémentation, cela peut varier).
  11. cnt a été post-incrémenté à des profondeurs R-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é.
  12. Avec main terminé avec succès en profondeur R-5, la pile entière se déroule sans que d'autres blocs catch soient exécutés et le programme se termine.
6
Craig Gidney

Je pense que le nombre affiché est le nombre de fois que l'appel System.out.println Lève l'exception Stackoverflow.

Cela dépend probablement de l'implémentation de println et du nombre d'appels d'empilement qu'il est effectué.

Pour illustrer:

L'appel main() déclenche l'exception Stackoverflow à l'appel i. L'appel i-1 de main intercepte l'exception et appelle println qui déclenche une seconde Stackoverflow. cnt passe à 1. L'appel i-2 de main catch maintenant l'exception et appelle println. Dans println une méthode est appelée déclenchant une troisième exception. cnt passe à 2. ceci continue jusqu'à ce que println puisse effectuer tous les appels nécessaires et finalement afficher la valeur de cnt.

Cela dépend alors de l'implémentation 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 une certaine marge de manœuvre à la logique de correction, soit l'implémentation println ne fait pas d'appels, soit le L'opération ++ est effectuée après l'appel println est donc contournée par l'exception.

6
Kazaag

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 sera lancé. En fait, la pile d'un thread Java stocke les cadres, qui contiennent toutes les données nécessaires pour appeler une méthode et reprendre. Selon Spécifications du langage Java pour Java 6 , lors de l'appel d'une méthode,

S'il n'y a pas suffisamment de mémoire disponible pour créer une telle trame d'activation, une StackOverflowError est levée.

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 Spécifications de la machine virtuelle Java pour Java 6 ,

les cadres peuvent être alloués en tas.

Ainsi, lorsqu'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 en invoquant System.out.println (peut) avoir besoin de 5 niveaux d'invocation de méthode, donc 5 cadres doivent être créés. Ensuite, lorsque StackOverflowError 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 utilisez cnt++. 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 de System.out.println est environ 51 fois de main, donc il revient 51 fois et imprime 50.

1
Jay

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

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

Ensuite, vous verrez toutes les valeurs de cnt et 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.

0
me_digvijay