简体   繁体   English

为什么最大递归深度我可以达到非确定性?

[英]Why is the max recursion depth I can reach non-deterministic?

I decided to try a few experiments to see what I could discover about the size of stack frames, and how far through the stack the currently executing code was. 我决定尝试一些实验,看看我能发现堆栈帧的大小,以及当前执行代码在堆栈中的距离。 There are two interesting questions we might investigate here: 我们可能会在这里调查两个有趣的问题:

  1. How many levels deep into the stack is the current code? 当前代码的堆栈深度是多少?
  2. How many levels of recursion can the current method reach before it hits a StackOverflowError ? 当前方法遇到StackOverflowError之前可以达到多少级别的递归?

Stack depth of currently executing code 堆栈当前执行代码的深度

Here's the best I could come up with for this: 这是我能想到的最好的:

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

This seems a bit hacky. 这看起来有点黑客。 It generates and catches an exception, and then looks to see what the length of the stack trace is. 它生成并捕获异常,然后查看堆栈跟踪的长度。

Unfortunately it also seems to have a fatal limitation, which is that the maximum length of the stack trace returned is 1024. Anything beyond that gets axed, so the maximum that this method can return is 1024. 不幸的是,它似乎也有一个致命的限制,即返回的堆栈跟踪的最大长度为1024.除此之外的任何内容都被削减,因此此方法可以返回的最大值为1024。

Question: 题:

Is there a better way of doing this that isn't so hacky and doesn't have this limitation? 有没有更好的方法做到这一点,不是那么hacky并没有这个限制?

For what it's worth, my guess is that there isn't: Throwable.getStackTraceDepth() is a native call, which suggests (but doesn't prove) that it can't be done in pure Java. 对于它的价值,我的猜测是没有: Throwable.getStackTraceDepth()是一个本机调用,它建议(但不能证明)它不能用纯Java完成。

Determining how much more recursion depth we have left 确定我们剩下多少递归深度

The number of levels we can reach will be determined by (a) size of stack frame, and (b) amount of stack left. 我们可以达到的等级数量将由(a)堆栈帧的大小和(b)剩余堆栈量确定。 Let's not worry about size of stack frame, and just see how many levels we can reach before we hit a StackOverflowError . 让我们不要担心堆栈帧的大小,只需看看在我们遇到StackOverflowError之前我们可以达到多少级别。

Here's my code for doing this: 这是我执行此操作的代码:

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

It does its job admirably, even if it's linear in the amount of stack remaining. 它的工作令人钦佩,即使它在剩余堆栈数量上是线性的。 But here is the very, very weird part. 但这是非常非常奇怪的部分。 On 64-bit Java 7 (OpenJDK 1.7.0_65), the results are perfectly consistent: 9,923, on my machine (Ubuntu 14.04 64-bit). 在64位Java 7(OpenJDK 1.7.0_65)上,结果完全一致:9,923,在我的机器上(Ubuntu 14.04 64位)。 But Oracle's Java 8 (1.8.0_25) gives me non-deterministic results : I get a recorded depth of anywhere between about 18,500 and 20,700. 但Oracle的Java 8(1.8.0_25)给了我非确定性的结果 :我得到的记录深度在18,500到20,700之间。

Now why on earth would it be non-deterministic? 现在为什么它是非确定性的呢? There's supposed to be a fixed stack size, isn't there? 应该有一个固定的堆栈大小,不是吗? And all of the code looks deterministic to me. 并且所有代码对我来说都是确定性的。

I wondered whether it was something weird with the error trapping, so I tried this instead: 我想知道错误捕获是否奇怪,所以我尝试了这个:

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

Clearly this will either return the input it was given, or overflow. 显然,这将返回给定的输入或溢出。

Again, the results I get are non-deterministic on Java 8. If I call badSum(14500) , it will give me a StackOverflowError about half the time, and return 14500 the other half. 同样,我得到的结果在Java 8上是非确定性的。如果我调用badSum(14500) ,它将给我一个StackOverflowError大约一半的时间,并返回14500另一半。 but on Java 7 OpenJDK, it's consistent: badSum(9160) completes fine, and badSum(9161) overflows. 但是在Java 7 OpenJDK上,它是一致的: badSum(9160)完成正常, badSum(9161)溢出。

Question: 题:

Why is the maximum recursion depth non-deterministic on Oracle's Java 8? 为什么Oracle Java 8上的最大递归深度不确定? And why is it deterministic on OpenJDK 7? 为什么OpenJDK 7确定性?

The observed behavior is affected by the HotSpot optimizer, however it is not the only cause. 观察到的行为受HotSpot优化器的影响,但它不是唯一的原因。 When I run the following code 当我运行以下代码时

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

with JIT enabled, I get results like: 启用JIT后,我得到的结果如下:

> 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

Here, the effect of the JIT is clearly visible, obviously the optimized code needs less stack space, and it's shown that tiered compilation is enabled (indeed, using -XX:-TieredCompilation shows a single jump if the program runs long enough). 在这里,JIT的效果清晰可见,显然优化的代码需要更少的堆栈空间,并且显示分层编译已启用(实际上,使用-XX:-TieredCompilation如果程序运行足够长则显示单个跳转)。

In contrast, with disabled JIT I get the following results: 相反,对于禁用的JIT,我得到以下结果:

> 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

The values still vary, but not within the single runtime thread and with a lesser magnitude. 值仍然有所不同,但不在单个运行时线程内且幅度较小。

So, there is a (rather small) difference that becomes much larger if the optimizer can reduce the stack space required per method invocation, eg due to inlining. 因此,如果优化器可以减少每个方法调用所需的堆栈空间(例如由于内联),则存在(相当小的)差异变得更大。

What can cause such a difference? 什么能造成这样的差异? I don't know how this JVM does it but one scenario could be that the way a stack limit is enforced requires a certain alignment of the stack end address (eg matching memory page sizes) while the memory allocation returns memory with a start address that has a weaker alignment guaranty. 我不知道这个JVM是如何做到的,但是一种情况可能是强制执行堆栈限制的方式需要堆栈结束地址的某种对齐(例如匹配内存页面大小),而内存分配返回内存的起始地址是对齐保证较弱。 Combine such a scenario with ASLR and there might be always a difference, within the size of the alignment requirement. 将这种情况与ASLR结合起来可能总是存在差异,在对齐要求的大小范围内。

Why is the maximum recursion depth non-deterministic on Oracle's Java 8? And why is it deterministic on OpenJDK 7?

About that, possibly relates to changes in garbage collection. 关于这一点,可能与垃圾收集的变化有关。 Java can choose a different mode for gc each time. Java每次都可以为gc选择不同的模式。 http://vaskoz.wordpress.com/2013/08/23/java-8-garbage-collectors/ http://vaskoz.wordpress.com/2013/08/23/java-8-garbage-collectors/

It's deprecated, but you could try Thread.countStackFrames() like 它已被弃用,但您可以尝试Thread.countStackFrames()类的

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

Per the Javadoc, 根据Javadoc,

Deprecated . 不推荐 The definition of this call depends on suspend() , which is deprecated. 此调用的定义取决于不推荐使用的suspend() Further, the results of this call were never well-defined. 此外,这次电话会议的结果从未明确定义过。

Counts the number of stack frames in this thread. 计算此线程中的堆栈帧数。 The thread must be suspended. 线程必须暂停。

As for why you observe non-deterministic behaviour, I can only assume it is some combination of the JIT and garbage collector. 至于为什么你观察到非确定性行为,我只能假设它是JIT和垃圾收集器的某种组合。

You don't need to catch the exception, just create one like this: 您不需要捕获异常,只需创建一个这样的:

new Throwable().getStackTrace() new Throwable()。getStackTrace()

Or: 要么:

Thread.currentThread().getStackTrace() Thread.currentThread()。的getStackTrace()

It's still hacky as the result is JVM implementation specific. 它仍然是hacky,因为结果是特定于JVM实现。 And JVM may decide to trim the result for better performance. JVM可能会决定调整结果以获得更好的性能。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM