Pourquoi la profondeur de récursion maximale que je peux atteindre est-elle non déterministe?

J'ai décidé d'essayer quelques expériences pour voir ce que je pouvais découvrir sur la taille des images de pile, et à quelle distance dans la pile le code en cours d'exécution était. Il y a deux questions intéressantes que nous pourrions examiner ici:

  1. combien de niveaux profondément dans la pile est le code actuel?
  2. combien de niveaux de récursivité la méthode actuelle peut-elle atteindre avant d'atteindre un StackOverflowError?

Profondeur de pile du code en cours d'exécution

Voici le meilleur I pourrait venir avec pour cela:

public static int levelsDeep() {
    try {
        throw new SomeKindOfException();
    } catch (SomeKindOfException e) {
        return e.getStackTrace().length;
    }
}

Cela semble un peu hacky. Il génère et attrape une exception, puis cherche à voir quelle est la longueur de la trace de la pile.

Malheureusement, il semble aussi avoir une limitation fatale, qui est que la longueur maximale de la trace de pile retournée est 1024. Tout ce qui est au-delà est éliminé, donc le maximum que cette méthode peut renvoyer est 1024.

Question:

y a-t-il une meilleure façon de faire cela qui ne l'est pas hacky et n'a pas cette limitation?

Pour ce que ça vaut, je suppose qu'il n'y en a pas: Throwable.getStackTraceDepth() est un appel natif, ce qui suggère (mais ne prouve pas) que cela ne peut pas être fait en Java pur.

Déterminer combien il nous reste de profondeur de récursivité

Le nombre de niveaux que nous pouvons atteindre sera déterminé par (A) la taille du cadre de la pile, et (b) la quantité de pile restante. Ne nous inquiétons pas de la taille du cadre de la pile, et il suffit de voir combien de niveaux nous pouvons atteindre avant de frapper un StackOverflowError.

Voici mon code pour faire ceci:

public static int stackLeft() {
    try {
        return 1+stackLeft();
    } catch (StackOverflowError e) {
        return 0;
    }
}

Il fait son travail admirablement, même s'il est linéaire dans la quantité de pile restante. Mais voici la partie très, très bizarre. Sur Java 7 64 bits (OpenJDK 1.7.0_65), les résultats sont parfaitement cohérents: 9,923, sur ma machine (Ubuntu 14.04 64 bits). Mais Java 8 D'Oracle (1.8.0_25) me donne des résultats non déterministes : j'obtiens une profondeur enregistrée comprise entre environ 18 500 et 20 700.

, Maintenant, pourquoi sur terre serait-il non déterministe? Il est censé y avoir une taille de pile fixe, n'est-ce pas? Et tout le code me semble déterministe.

Je me demandais si c'était quelque chose de bizarre avec le piégeage d'erreur, alors j'ai essayé ceci à la place:

public static long badSum(int n) {
    if (n==0)
        return 0;
    else
        return 1+badSum(n-1);
}

Clairement, cela retournera soit l'entrée qui lui a été donnée, soit le débordement.

Encore une fois, les résultats que j'obtiens sont non déterministes sur Java 8. Si j'appelle badSum(14500), Il me donnera un StackOverflowError environ la moitié du temps, et retournera 14500 l'autre moitié. mais sur Java 7 OpenJDK, c'est cohérent: badSum(9160) se termine bien, et badSum(9161) déborde.

Question:

pourquoi la profondeur de récursivité maximale est-elle non déterministe sur Java 8 D'Oracle? Et pourquoi est-ce déterministe sur OpenJDK 7?
26
demandé sur chiastic-security 2014-11-20 18:56:39

4 réponses

Le comportement observé est affecté par L'optimiseur de HotSpot, mais ce n'est pas la seule cause. Quand j'exécute le code suivant

public static void main(String[] argv) {
    System.out.println(System.getProperty("java.version"));
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
    System.out.println(countDepth());
}
static int countDepth() {
    try { return 1+countDepth(); }
    catch(StackOverflowError err) { return 0; }
}

Avec JIT activé, j'obtiens des résultats comme:

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -cp build\classes X
1.8.0_40-ea
2097
4195
4195
4195
12587
12587
12587

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -cp build\classes X
1.8.0_40-ea
2095
4193
4193
4193
12579
12579
12579

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -cp build\classes X
1.8.0_40-ea
2087
4177
4177
12529
12529
12529
12529

Ici, l'effet du JIT est clairement visible, évidemment le code optimisé a besoin de moins d'espace de pile, et il est montré que la compilation à plusieurs niveaux est activée (en effet, l'utilisation de -XX:-TieredCompilation montre un seul saut si le programme s'exécute assez longtemps).

En revanche, avec JIT désactivé, j'obtiens ce qui suit résultats:

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -Xint -cp build\classes X
1.8.0_40-ea
2104
2104
2104
2104
2104
2104
2104

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -Xint -cp build\classes X
1.8.0_40-ea
2076
2076
2076
2076
2076
2076
2076

> f:\Software\jdk1.8.0_40beta02\bin\java -Xss68k -server -Xint -cp build\classes X
1.8.0_40-ea
2105
2105
2105
2105
2105
2105
2105

Les valeurs varient toujours, mais pas dans le thread d'exécution unique et avec une magnitude moindre.

Donc, il y a une différence (plutôt petite) qui devient beaucoup plus grande si l'optimiseur peut réduire l'espace de pile requis par invocation de méthode, par exemple en raison de l'inlining.

Ce qui peut causer une telle différence? Je ne sais pas comment cette JVM le fait mais un scénario pourrait être que la façon dont une limite de pile est appliquée nécessite un certain alignement de la pile end adresse (par exemple, les tailles de page de mémoire correspondant) tandis que la mémoire allocation renvoie la mémoire avec une adresse de départ qui a une garantie d'alignement plus faible. Combinez un tel scénario avec ASLR et il pourrait y avoir toujours une différence, dans la taille de l'exigence d'alignement.

13
répondu Holger 2014-11-20 19:22:08
Why is the maximum recursion depth non-deterministic on Oracle's Java 8? And why is it deterministic on OpenJDK 7?

À ce sujet, peut-être se rapporte à des changements dans la collecte des ordures. Java peut choisir un mode différent pour gc à chaque fois. http://vaskoz.wordpress.com/2013/08/23/java-8-garbage-collectors/

1
répondu Pedro Montoto García 2014-11-20 16:12:17

, Il est obsolète, mais vous pouvez essayer de Thread.countStackFrames() comme

public static int levelsDeep() {
    return Thread.currentThread().countStackFrames();       
}

Par Javadoc,

Obsolète. La définition de cet appel dépend de suspend(), qui est obsolète. De plus, les résultats de cet appel n'ont jamais été bien définis.

Compte le nombre d'images de pile dans ce thread. Le fil doit être suspendu.

Quant à la raison pour laquelle vous observez un comportement non déterministe, Je ne peux que supposer que c'est une combinaison du JIT et garbage collector.

1
répondu Elliott Frisch 2014-11-20 16:14:25

Vous n'avez pas besoin d'attraper l'exception, il suffit de créer une comme ceci:

Nouveau Throwable ().getStackTrace ()

Ou:

Thread.currentThread ().getStackTrace ()

C'est toujours hacky car le résultat est spécifique à L'implémentation de la JVM. Et JVM peut décider de couper le résultat pour de meilleures performances.

1
répondu Chris 2014-12-19 09:33:04