Comment augmenter la taille de la pile Java?

123

J'ai posé cette question pour savoir comment augmenter la taille de la pile d'appels d'exécution dans la JVM. J'ai une réponse à cela, et j'ai également de nombreuses réponses et commentaires utiles sur la façon dont Java gère la situation où une grande pile d'exécution est nécessaire. J'ai prolongé ma question avec le résumé des réponses.

À l'origine, je voulais augmenter la taille de la pile JVM afin que les programmes comme s'exécutent sans StackOverflowError.

public class TT {
  public static long fact(int n) {
    return n < 2 ? 1 : n * fact(n - 1);
  }
  public static void main(String[] args) {
    System.out.println(fact(1 << 15));
  }
}

Le paramètre de configuration correspondant est l' java -Xss...indicateur de ligne de commande avec une valeur suffisamment grande. Pour le programme TTci-dessus, cela fonctionne comme ceci avec la JVM d'OpenJDK:

$ javac TT.java
$ java -Xss4m TT

L'une des réponses a également souligné que les -X...indicateurs dépendent de la mise en œuvre. J'utilisais

java version "1.6.0_18"
OpenJDK Runtime Environment (IcedTea6 1.8.1) (6b18-1.8.1-0ubuntu1~8.04.3)
OpenJDK 64-Bit Server VM (build 16.0-b13, mixed mode)

Il est également possible de spécifier une grande pile pour un seul thread (voir dans l'une des réponses comment). Ceci est recommandé java -Xss...pour éviter de gaspiller de la mémoire pour les threads qui n'en ont pas besoin.

J'étais curieux de savoir quelle était la taille exacte d'une pile dont le programme ci-dessus avait besoin, alors je l'ai exécuté en naugmentant:

  • -Xss4m peut suffire pour fact(1 << 15)
  • -Xss5m peut suffire pour fact(1 << 17)
  • -Xss7m peut suffire pour fact(1 << 18)
  • -Xss9m peut suffire pour fact(1 << 19)
  • -Xss18m peut suffire pour fact(1 << 20)
  • -Xss35m peut suffire pour fact(1 << 21)
  • -Xss68m peut suffire pour fact(1 << 22)
  • -Xss129m peut suffire pour fact(1 << 23)
  • -Xss258m peut suffire pour fact(1 << 24)
  • -Xss515m peut suffire pour fact(1 << 25)

D'après les chiffres ci-dessus, il semble que Java utilise environ 16 octets par image de pile pour la fonction ci-dessus, ce qui est raisonnable.

L'énumération ci-dessus contient peut être suffisante au lieu d' être suffisante , car l'exigence de pile n'est pas déterministe: l'exécuter plusieurs fois avec le même fichier source et le même -Xss...réussit parfois et donne parfois un fichier StackOverflowError. Par exemple, pour 1 << 20, -Xss18mc'était suffisant en 7 courses sur 10, et ce -Xss19mn'était pas toujours suffisant non plus, mais -Xss20mc'était suffisant (au total 100 courses sur 100). Le garbage collection, le démarrage du JIT ou autre chose provoquent-ils ce comportement non déterministe?

La trace de pile imprimée à un StackOverflowError(et peut-être aussi à d'autres exceptions) montre uniquement les 1024 éléments les plus récents de la pile d'exécution. Une réponse ci-dessous montre comment compter la profondeur exacte atteinte (qui peut être beaucoup plus grande que 1024).

De nombreuses personnes qui ont répondu ont souligné que c'est une bonne et sûre pratique de codage d'envisager des implémentations alternatives, moins gourmandes en pile, du même algorithme. En général, il est possible de convertir un ensemble de fonctions récursives en fonctions itératives (en utilisant un Stackobjet par exemple , qui est rempli sur le tas au lieu de sur la pile d'exécution). Pour cette factfonction particulière , il est assez facile de la convertir. Ma version itérative ressemblerait à:

public class TTIterative {
  public static long fact(int n) {
    if (n < 2) return 1;
    if (n > 65) return 0;  // Enough powers of 2 in the product to make it (long)0.
    long f = 2;
    for (int i = 3; i <= n; ++i) {
      f *= i;
    }
    return f;
  }
  public static void main(String[] args) {
    System.out.println(fact(1 << 15));
  }
}

Pour info, comme le montre la solution itérative ci-dessus, la factfonction ne peut pas calculer la factorielle exacte des nombres supérieurs à 65 (en fait, même supérieurs à 20), car le type intégré Java longdéborderait. La refactorisation factafin qu'elle renvoie un BigIntegerau lieu de longproduirait également des résultats exacts pour les entrées importantes.

points
la source
Ça a l'air plus simple qu'il ne l'est. fact () est appelé 32K fois de manière récursive. Cela devrait être inférieur à 1 Mo de pile. : - /
Aaron Digulla
@Aaron: + Fonction supplémentaire, ce qui est .. BEAUCOUP
halfdan
4
Mis à part vos problèmes de pile. notez que vous faites exploser vos longs et intimes. 1 << 4 est la valeur maximale que je peux utiliser avant de passer au négatif, puis à 0. Essayez d'utiliser BigInteger
Sean
Je ne suis pas sûr que la surcharge de la fonction soit réellement importante - je pense que vous devriez toujours être capable de faire 2 ^ 15 appels de l'ordre de quelques mégaoctets d'espace de pile.
Neil Coffey
7
Remarque: vous définissez la taille de pile de chaque thread et produisez un résultat dénué de sens, le tout pour éviter de refactoriser une ligne de code. Je suis heureux que vous ayez réglé vos priorités. : P
Peter Lawrey

Réponses:

78

Hmm ... cela fonctionne pour moi et avec beaucoup moins de 999 Mo de pile:

> java -Xss4m Test
0

(Windows JDK 7, machine virtuelle cliente build 17.0-b05 et Linux JDK 6 - mêmes informations de version que celles que vous avez publiées)

Jon Skeet
la source
1
c'était probablement pour mon commentaire, je l'ai supprimé quand j'ai réalisé la même chose que Neil avait posté.
Sean
Grâce à cette question et à votre réponse, j'ai réussi à terminer ma mission. Ma fonction DFS devait récurer sur un graphe avec ~ 10 ^ 5 sommets. Enfin, cela a fonctionné avec -Xss129m: D
bholagabbar
11

Je suppose que vous avez calculé la "profondeur de 1024" par les lignes récurrentes dans la trace de pile?

De toute évidence, la longueur du tableau de trace de pile dans Throwable semble être limitée à 1024. Essayez le programme suivant:

public class Test {

    public static void main(String[] args) {

        try {
            System.out.println(fact(1 << 15));
        }
        catch (StackOverflowError e) {
            System.err.println("true recursion level was " + level);
            System.err.println("reported recursion level was " +
                               e.getStackTrace().length);
        }
    }

    private static int level = 0;
    public static long fact(int n) {
        level++;
        return n < 2 ? n : n * fact(n - 1);
    }
}
Geai
la source
9

Si vous voulez jouer avec la taille de la pile de threads, vous voudrez regarder l'option -Xss sur la JVM Hotspot. Cela peut être différent sur les machines virtuelles non Hotspot puisque les paramètres -X de la JVM sont spécifiques à la distribution, IIRC.

Sur Hotspot, cela ressemble à java -Xss16Msi vous voulez faire une taille de 16 Mo.

Type java -X -help si vous voulez voir tous les paramètres JVM spécifiques à la distribution que vous pouvez transmettre. Je ne sais pas si cela fonctionne de la même manière sur les autres JVM, mais cela imprime tous les paramètres spécifiques à Hotspot.

Pour ce que ça vaut - je recommanderais de limiter votre utilisation des méthodes récursives en Java. Ce n'est pas trop génial pour les optimiser - pour l'un, la JVM ne prend pas en charge la récursivité de queue (voir La JVM empêche-t - elle les optimisations des appels de queue? ). Essayez de refactoriser votre code factoriel ci-dessus pour utiliser une boucle while au lieu d'appels de méthode récursifs.

whaley
la source
8

Le seul moyen de contrôler la taille de la pile au sein du processus est d'en démarrer un nouveau Thread. Mais vous pouvez également contrôler en créant un sous-processus Java auto-appelant avec le -Xssparamètre.

public class TT {
    private static int level = 0;

    public static long fact(int n) {
        level++;
        return n < 2 ? n : n * fact(n - 1);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(null, null, "TT", 1000000) {
            @Override
            public void run() {
                try {
                    level = 0;
                    System.out.println(fact(1 << 15));
                } catch (StackOverflowError e) {
                    System.err.println("true recursion level was " + level);
                    System.err.println("reported recursion level was "
                            + e.getStackTrace().length);
                }
            }

        };
        t.start();
        t.join();
        try {
            level = 0;
            System.out.println(fact(1 << 15));
        } catch (StackOverflowError e) {
            System.err.println("true recursion level was " + level);
            System.err.println("reported recursion level was "
                    + e.getStackTrace().length);
        }
    }

}
Dennis C
la source
Merci pour cette réponse informative, il est bon de connaître les options en plus de java -Xss....
pts
1
J'étais excité à ce sujet, mais après avoir lu docs.oracle.com/javase/6/docs/api/java/lang/Thread.html#Thread - le constructeur stacksize - l'excitation est partie.
kellogs
Je me demande quelles plates-formes sont-ils lorsque le document dit seulement - "Sur certaines plates-formes"
Dennis C
3

Ajouter cette option

--driver-java-options -Xss512m

à votre commande spark-submit résoudra ce problème.

Guibin Zhang
la source
2

Il est difficile de donner une solution sensée car vous souhaitez éviter toutes les approches saines. Refactoriser une ligne de code est la solution sensible.

Remarque: l'utilisation de -Xss définit la taille de la pile de chaque thread et est une très mauvaise idée.

Une autre approche consiste à manipuler le code d'octet pour modifier le code comme suit;

public static long fact(int n) { 
    return n < 2 ? n : n > 127 ? 0 : n * fact(n - 1); 
}

étant donné que chaque réponse pour n> 127 est 0. Cela évite de changer le code source.

Peter Lawrey
la source
1
Merci de souligner que définir une taille de pile élevée gaspillerait de la mémoire pour les threads qui n'en ont pas besoin. Merci également de souligner que la factfonction de la question peut être refactorisée pour utiliser beaucoup moins d'espace dans la pile.
pts
1
@pts, vos remerciements sont notés. Je pense que c'est une question sensée étant donné un cas d'utilisation beaucoup plus complexe, mais ceux-ci sont très rares.
Peter Lawrey
0

Bizarre! Vous dites que vous voulez générer une récursion de 1 << 15 profondeur ??? !!!!

Je suggère de ne pas l'essayer. La taille de la pile sera 2^15 * sizeof(stack-frame). Je ne sais pas quelle est la taille du stack-frame, mais 2 ^ 15 vaut 32.768. À peu près ... Eh bien, s'il s'arrête à 1024 (2 ^ 10), vous devrez le rendre 2 ^ 5 fois plus grand, c'est 32 fois plus grand qu'avec votre réglage actuel.

Hélios
la source
0

D'autres affiches ont indiqué comment augmenter la mémoire et que vous pouvez mémoriser les appels. Je suggère que pour de nombreuses applications, vous pouvez utiliser la formule de Stirling pour approcher un grand n! très rapidement avec presque aucune empreinte mémoire.

Jetez un coup d'œil à cet article, qui présente une analyse de la fonction et du code:

http://threebrothers.org/brendan/blog/stirlings-approximation-formula-clojure/

fuite
la source
0

J'ai fait un excersize Anagram , ce qui est comme un problème de Count Change mais avec 50 000 dénominations (pièces de monnaie). Je ne suis pas sûr que cela puisse être fait de manière itérative , je m'en fiche. Je sais juste que l'option -xss n'avait aucun effet - j'ai toujours échoué après 1024 images de pile (peut-être que scala fait un mauvais travail en livrant à java ou à la limitation de printStackTrace. Je ne sais pas). C'est une mauvaise option, comme expliqué de toute façon. Vous ne voulez pas que tous les threads de l'application soient monstrueux. Cependant, j'ai fait quelques expériences avec le nouveau Thread (taille de pile). Cela fonctionne en effet,

  def measureStackDepth(ss: Long): Long = {
    var depth: Long = 0
      val thread: Thread = new Thread(null, new Runnable() {
        override def run() {
          try {
          def sum(n: Long): Long = {depth += 1; if (n== 0) 0 else sum(n-1) + 1}
          println("fact = " + sum(ss * 10))
          } catch {
            case e: StackOverflowError => // eat the exception, that is expected
          }
        }
      }, "deep stack for money exchange", ss)
      thread.start()
      thread.join()
    depth
  }                                               //> measureStackDepth: (ss: Long)Long


  for (ss <- (0 to 10)) println("ss = 10^" +  ss + " allows stack of size " -> measureStackDepth((scala.math.pow (10, ss)).toLong) )
                                                  //> fact = 10
                                                  //| (ss = 10^0 allows stack of size ,11)
                                                  //| fact = 100
                                                  //| (ss = 10^1 allows stack of size ,101)
                                                  //| fact = 1000
                                                  //| (ss = 10^2 allows stack of size ,1001)
                                                  //| fact = 10000
                                                  //| (ss = 10^3 allows stack of size ,10001)
                                                  //| (ss = 10^4 allows stack of size ,1336)
                                                  //| (ss = 10^5 allows stack of size ,5456)
                                                  //| (ss = 10^6 allows stack of size ,62736)
                                                  //| (ss = 10^7 allows stack of size ,623876)
                                                  //| (ss = 10^8 allows stack of size ,6247732)
                                                  //| (ss = 10^9 allows stack of size ,62498160)

Vous voyez que la pile peut croître de manière exponentielle plus profonde avec exponentiellement plus de pile allouée au thread.

Val
la source