Wie kann ich die Größe des Java-Stacks erhöhen?

123

Ich habe diese Frage gestellt, um zu erfahren, wie die Größe des Laufzeitaufrufstapels in der JVM erhöht werden kann. Ich habe eine Antwort darauf und ich habe auch viele nützliche Antworten und Kommentare, die relevant dafür sind, wie Java mit der Situation umgeht, in der ein großer Laufzeitstapel benötigt wird. Ich habe meine Frage mit der Zusammenfassung der Antworten erweitert.

Ursprünglich wollte ich die Größe des JVM-Stacks erhöhen, damit Programme wie a ohne laufen 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));
  }
}

Die entsprechende Konfigurationseinstellung ist das java -Xss...Befehlszeilenflag mit einem ausreichend großen Wert. Für das TTobige Programm funktioniert es mit OpenJDKs JVM folgendermaßen:

$ javac TT.java
$ java -Xss4m TT

Eine der Antworten hat auch darauf hingewiesen, dass die -X...Flags implementierungsabhängig sind. Ich habe benutzt

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)

Es ist auch möglich, einen großen Stapel nur für einen Thread anzugeben (siehe in einer der Antworten, wie). Dies wird empfohlen, um java -Xss...zu vermeiden, dass Speicher für Threads verschwendet wird, die ihn nicht benötigen.

Ich war neugierig, wie groß ein Stapel ist, den das obige Programm genau benötigt, also habe ich ihn nvergrößert ausgeführt:

  • -Xss4m kann ausreichen für fact(1 << 15)
  • -Xss5m kann ausreichen für fact(1 << 17)
  • -Xss7m kann ausreichen für fact(1 << 18)
  • -Xss9m kann ausreichen für fact(1 << 19)
  • -Xss18m kann ausreichen für fact(1 << 20)
  • -Xss35m kann ausreichen für fact(1 << 21)
  • -Xss68m kann ausreichen für fact(1 << 22)
  • -Xss129m kann ausreichen für fact(1 << 23)
  • -Xss258m kann ausreichen für fact(1 << 24)
  • -Xss515m kann ausreichen für fact(1 << 25)

Aus den obigen Zahlen geht hervor, dass Java für die obige Funktion ungefähr 16 Bytes pro Stapelrahmen verwendet, was vernünftig ist.

Die obige Aufzählung kann genug sein, anstatt genug zu sein , da die Stapelanforderung nicht deterministisch ist: Sie wird mehrmals mit derselben Quelldatei ausgeführt, und dieselbe ist -Xss...manchmal erfolgreich und ergibt manchmal eine StackOverflowError. ZB für 1 << 20, -Xss18mwar genug in 7 von 10 Läufen und -Xss19mwar auch nicht immer genug, aber -Xss20mwar genug (in allen 100 Läufen von 100). Verursacht die Speicherbereinigung, das Einschalten der JIT oder etwas anderes dieses nicht deterministische Verhalten?

Die Stapelverfolgung, die bei a StackOverflowError(und möglicherweise auch bei anderen Ausnahmen) gedruckt wird, zeigt nur die neuesten 1024 Elemente des Laufzeitstapels. Eine Antwort unten zeigt, wie die genaue erreichte Tiefe gezählt wird (die viel größer als 1024 sein kann).

Viele Leute, die geantwortet haben, haben darauf hingewiesen, dass es eine gute und sichere Codierungspraxis ist, alternative, weniger stapelhungrige Implementierungen desselben Algorithmus in Betracht zu ziehen. Im Allgemeinen ist es möglich, in eine Reihe von rekursiven Funktionen in iterative Funktionen umzuwandeln (unter Verwendung eines z. B. StackObjekts, das auf dem Heap anstatt auf dem Laufzeitstapel aufgefüllt wird). Für diese spezielle factFunktion ist es ziemlich einfach, sie zu konvertieren. Meine iterative Version würde folgendermaßen aussehen:

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));
  }
}

Zu Ihrer Information, wie die obige iterative Lösung zeigt, kann die factFunktion die exakte Fakultät von Zahlen über 65 (sogar über 20) nicht berechnen, da der in Java integrierte Typ longüberlaufen würde. Ein Refactoring fact, bei dem ein BigIntegerstatt zurückgegeben wird, longwürde auch bei großen Eingaben genaue Ergebnisse liefern.

pts
quelle
Sieht einfacher aus als es ist. fact () wird 32K-mal rekursiv aufgerufen. Das sollte weniger als 1 MB Stapel sein. : - /
Aaron Digulla
@ Aaron: + Funktionsaufwand, der ist .. eine
Menge
4
Abgesehen von Ihren Stapelproblemen. Beachten Sie, dass Sie Ihre Long und Ints in die Luft jagen. 1 << 4 ist der Maximalwert, den ich verwenden kann, bevor ich negativ werde und dann in 0. Versuchen Sie es mit BigInteger
Sean
Ich bin mir nicht sicher, ob der Funktionsaufwand tatsächlich sehr hoch ist. Ich denke, Sie sollten immer noch in der Lage sein, 2 ^ 15 Aufrufe in der Größenordnung von einigen Megabyte Stapelspeicher zu tätigen.
Neil Coffey
7
Hinweis: Sie legen die Stapelgröße für jeden Thread fest und erzielen ein bedeutungsloses Ergebnis, um zu vermeiden, dass eine Codezeile umgestaltet wird. Ich bin froh, dass Sie Ihre Prioritäten geklärt haben. : P
Peter Lawrey

Antworten:

78

Hmm ... es funktioniert bei mir und mit weit weniger als 999 MB Stack:

> java -Xss4m Test
0

(Windows JDK 7, Build 17.0-b05-Client-VM und Linux JDK 6 - dieselben Versionsinformationen wie Sie veröffentlicht haben)

Jon Skeet
quelle
1
höchstwahrscheinlich war es für meinen Kommentar, ich habe ihn gelöscht, als ich das Gleiche bemerkte, wie Neil es gepostet hat.
Sean
Dank dieser Frage und Ihrer Antwort konnte ich meine Aufgabe erfüllen. Meine DFS-Funktion musste in einem Diagramm mit ~ 10 ^ 5 Eckpunkten wiederkehren. Schließlich funktionierte es mit -Xss129m: D
bholagabbar
11

Ich nehme an, Sie haben die "Tiefe von 1024" anhand der wiederkehrenden Linien in der Stapelspur berechnet.

Offensichtlich scheint die Länge des Stack-Trace-Arrays in Throwable auf 1024 begrenzt zu sein. Versuchen Sie das folgende Programm:

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);
    }
}
Jay
quelle
9

Wenn Sie mit der Thread-Stapelgröße spielen möchten, sollten Sie sich die Option -Xss in der Hotspot-JVM ansehen. Bei Nicht-Hotspot-VMs kann dies etwas anderes sein, da die -X-Parameter für die JVM verteilungsspezifisch sind (IIRC).

Auf Hotspot sieht dies so aus, als java -Xss16Mob Sie die Größe 16 Megabyte erreichen möchten.

Art java -X -help Sie ein, wenn Sie alle verteilungsspezifischen JVM-Parameter anzeigen möchten, die Sie übergeben können. Ich bin nicht sicher, ob dies bei anderen JVMs gleich funktioniert, druckt jedoch alle Hotspot-spezifischen Parameter.

Für das, was es wert ist - ich würde empfehlen, die Verwendung rekursiver Methoden in Java einzuschränken. Es ist nicht besonders gut, sie zu optimieren - zum einen unterstützt die JVM keine Tail-Rekursion (siehe Verhindert die JVM Tail-Call-Optimierungen? ). Versuchen Sie, Ihren oben genannten Fakultätscode umzugestalten, um anstelle von rekursiven Methodenaufrufen eine while-Schleife zu verwenden.

Whaley
quelle
8

Die einzige Möglichkeit, die Größe des Stapels innerhalb eines Prozesses zu steuern, besteht darin, einen neuen zu starten Thread. Sie können dies jedoch auch steuern, indem Sie mit dem -XssParameter einen selbstaufrufenden Sub-Java-Prozess erstellen .

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.
quelle
Vielen Dank für diese informative Antwort, es ist schön, zusätzlich zu Optionen zu wissen java -Xss....
Punkte
1
Ich war begeistert davon, aber nachdem ich docs.oracle.com/javase/6/docs/api/java/lang/Thread.html#Thread - den Stapelgrößenkonstruktor - gelesen hatte, ging die Aufregung weg.
Kellogs
Ich frage mich, welche Plattformen sie sind, wenn das Dokument nur sagt - "Auf einigen Plattformen"
Dennis C
3

Fügen Sie diese Option hinzu

--driver-java-options -Xss512m

Mit Ihrem Spark-Submit-Befehl wird dieses Problem behoben.

Guibin Zhang
quelle
2

Es ist schwierig, eine vernünftige Lösung zu finden, da Sie alle vernünftigen Ansätze vermeiden möchten. Das Refactoring einer Codezeile ist die vernünftige Lösung.

Hinweis: Die Verwendung von -Xss legt die Stapelgröße jedes Threads fest und ist eine sehr schlechte Idee.

Ein anderer Ansatz ist die Manipulation von Bytecode, um den Code wie folgt zu ändern;

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

Jede Antwort für n> 127 ist 0. Dadurch wird eine Änderung des Quellcodes vermieden.

Peter Lawrey
quelle
1
Vielen Dank für den Hinweis, dass das Festlegen einer hohen Stapelgröße Speicher für Threads verschwenden würde, die ihn nicht benötigen. Vielen Dank auch für den Hinweis, dass die factFunktion in der Frage überarbeitet werden kann, um viel weniger Stapelspeicherplatz zu verbrauchen.
Punkte
1
@pts, dein Dank wird zur Kenntnis genommen. Ich denke, dies ist eine vernünftige Frage angesichts eines viel komplexeren Anwendungsfalls, aber diese sind sehr selten.
Peter Lawrey
0

Seltsam! Sie sagen, dass Sie eine Rekursion von 1 << 15 Tiefe erzeugen möchten ??? !!!!

Ich würde vorschlagen, es NICHT zu versuchen. Die Größe des Stapels wird sein 2^15 * sizeof(stack-frame). Ich weiß nicht, was Stack-Frame-Größe ist, aber 2 ^ 15 ist 32.768. Ziemlich genau ... Nun, wenn es bei 1024 (2 ^ 10) stoppt, müssen Sie es 2 ^ 5 mal größer machen, es ist 32 mal größer als bei Ihrer tatsächlichen Einstellung.

Helios
quelle
0

Andere Poster haben gezeigt, wie man den Speicher vergrößert und wie man sich Anrufe merken kann. Ich würde vorschlagen, dass Sie für viele Anwendungen die Stirling-Formel verwenden können, um das große n zu approximieren! sehr schnell mit fast keinem Speicherbedarf.

Werfen Sie einen Blick auf diesen Beitrag, der eine Analyse der Funktion und des Codes enthält:

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

Flucht
quelle
0

Ich habe Anagram exersize gemacht , was wie das Count Change- Problem ist, aber mit 50 000 Stückelungen (Münzen). Ich bin nicht sicher, ob es iterativ gemacht werden kann, es ist mir egal. Ich weiß nur, dass die Option -xss keine Auswirkung hatte - ich bin immer nach 1024 Stapelrahmen fehlgeschlagen (möglicherweise leistet Scala schlechte Arbeit bei der Lieferung an Java oder die Einschränkung von printStackTrace. Ich weiß es nicht). Dies ist eine schlechte Option, wie sowieso erklärt. Sie möchten nicht, dass alle Threads in der App monströs sind. Ich habe jedoch einige Experimente mit neuem Thread (Stapelgröße) durchgeführt. Das funktioniert in der Tat,

  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)

Sie sehen, dass der Stapel exponentiell tiefer wachsen kann, wenn dem Thread exponentiell mehr Stapel zugewiesen werden.

Val
quelle