简体   繁体   English

递归方法的间歇堆栈溢出

[英]Intermittent Stack Overflow for Recursion Method

I have a simple method I've written for a class homework assignment that uses recursion (yes, it must use recursion) to calculate the number of triangles in a fractal pattern: 我有一个简单的方法,我为类作业分配编写,使用递归(是的,它必须使用递归)来计算分形图案中的三角形数量:

public static BigInteger triangleFract(int layer) {
    if(layer < 0) {
        throw new IllegalArgumentException("Input must be >= 0");
    } else if(layer == 0) {
        return new BigInteger("0");
    } else if (layer == 1) {
        return new BigInteger("1");
    } else {
        return triangleFract(layer - 1)
              .multiply(new BigInteger("3"))
              .add(new BigInteger("2"));
    }
}

What I've been trying to do is understand how big the int layer can be so as to limit user input. 我一直在努力做的是了解int层的大小,以限制用户输入。 After some tests I get a stack overflow at around 6700+, which is fine. 经过一些测试后,我得到了大约6700+的堆栈溢出,这很好。

What is troubling me is that if layer is in the thousands, the method usually runs, but it can still randomly encounter a StackOverflowError . 令我不安的是,如果图层数以千计,则该方法通常会运行,但它仍然可以随机遇到StackOverflowError

For instance, I chose to limit layer to 4444, and it seems to be able to handle that almost always, but every once in a while it still seems to overflow. 例如,我选择将图层限制为4444,并且它似乎能够几乎总是处理它,但每隔一段时间它似乎仍然会溢出。

Why does it do this? 为什么这样做? And is there anything that I can do about it? 我能做些什么吗?

Consider to move to iterated version. 考虑转向迭代版本。 That what I think if you developing a recursion algorithm you must control the level depth or don't use recursion at all. 我认为,如果你开发一个递归算法,你必须控制水平深度或根本不使用递归。

Perhaps the JVM has determined (through escape analysis) that the BigInteger can be allocated on the stack rather than the heap. 也许JVM已经确定(通过转义分析)可以在堆栈而不是堆上分配BigInteger。 Depending on when it implements this optimization, the required stack size would vary. 根据实现此优化的时间,所需的堆栈大小会有所不同。

That said, there could be many other causes, and the behaviour is likely to depend on the JVM you use. 也就是说,可能还有很多其他原因,而且行为很可能取决于您使用的JVM。

Allowing recursion to that depth is a design smell. 允许递归到那个深度是一种设计气味。

Try this iterative version: 试试这个迭代版本:

public static BigInteger triangleFract(int layer) {
    if (layer < 0) {
        throw new IllegalArgumentException("Input must be >= 0");
    }
    if (layer == 0) {
        return BigInteger.ZERO;
    }
    BigInteger result = BigInteger.ONE;
    BigInteger two = new BigInteger("2");
    BigInteger three = new BigInteger("3");
    for (int i = 1; i < layer; i++) {
        result = result.multiply(three).add(two);
    }
    return result;
}

Notes: 笔记:

  • Using BigInteger.ZERO and BigInteger.ONE instead of creating new instances for these values 使用BigInteger.ZEROBigInteger.ONE而不是为这些值创建新实例
  • Removed redundant else - there is no else after a terminating statement (eg return ) 删除多余的else -有没有 else终止声明(如后return
  • Re-using new BigInteger("2") and new BigInteger("3") rather than creating new instances every iteration 重新使用new BigInteger("2")new BigInteger("3")而不是每次迭代都创建新的实例

There is actually something you can do: increase the maximum stack size. 实际上你可以做些什么:增加最大堆栈大小。 This is done at JVM startup with the option -Xss like this: 这是在JVM启动时使用选项-Xss完成的,如下所示:

java -Xss40m MainClass

Be careful not to set an excessively high value. 小心不要设置过高的值。 If you have to go over 60M - 70M, then a redesign of your code is recommended. 如果你必须超过60M - 70M,那么建议重新设计你的代码。

I couldn't reproduce your 'fluctuation' effect. 我无法重现你的“波动”效应。 This is pretty deterministic code, so you should get the same result every time (including the stack overflow error). 这是非常确定的代码,因此每次都应该得到相同的结果(包括堆栈溢出错误)。

How did you test it? 你是怎么测试的? Did you run a new jvm for each try of your 4444 test? 你为4444测试的每次尝试都运行了一个新的jvm吗? (or was it just the triangleFrac(4444); called in a loop?). (或者它只是triangleFrac(4444);在循环中调用?)。

What's your OS, java version etc... 什么是你的操作系统,java版本等......

I'm asking because I don't really like unsolved problems like this --- something like this could bite you where (and when) it hurts ;). 我问,因为我不喜欢像这样的未解决的问题 - 这样的事情可能会让你感到疼痛的地方(以及何时);)。

oh... BTW, for all it's worth, you should be using the ONE and ZERO constants from BigInteger (and create your own for 2 and 3, for that matter. This should save you quite a bit of memory (yes, I know, this wasn't your question). 哦...顺便说一句,尽管它值得,你应该使用BigInteger中的ONE和ZERO常量(并为此创建你自己的2和3。这应该可以节省你很多内存(是的,我知道) ,这不是你的问题)。

For those who can't reproduce this fluctuation. 对于那些无法重现这种波动的人。 Find the layer value starting from which method will reliably throw StackOverflowError . 从哪个方法可靠地抛出StackOverflowError开始查找layer值。 The closer this value to the real threshold the better. 该值越接近真实阈值越好。 Now call this method from inside the loop (on my machine maxLayer = 11500 ): 现在从循环内部调用此方法(在我的机器上maxLayer = 11500 ):

int i = 11500;
while (true) {
    System.out.println(i);
    triangleFract(i++);
}

It will throw StackOverflowError . 它将抛出StackOverflowError Now decrease this value a little bit (looks like 5-10% should be enough): 现在稍微降低这个值(看起来5-10%应该足够了):

int i = 10500;
while (true) {
    System.out.println(i);
    triangleFract(i++);
}

Well on my machine this doesn't throw any error and successfully jumps over 11500 . 在我的机器上,这不会抛出任何错误,并成功跳过11500 Actually it works fine up until 16000 . 实际上它一直工作到1 16000

So whatever it is it probably involves JVM optimisations. 无论它是什么,它可能涉及JVM优化。 I tried to run a program with -XX:+PrintCompilation . 我尝试使用-XX:+PrintCompilation运行程序。 I saw how JIT does its job while looping: 我看到JIT在循环时如何完成它的工作:

 117 1 java.lang.String::hashCode (64 bytes) 183 2 java.lang.String::charAt (33 bytes) 189 3 sun.nio.cs.UTF_8$Decoder::decodeArrayLoop (553 bytes) 201 4 java.math.BigInteger::mulAdd (81 bytes) 205 5 java.math.BigInteger::multiplyToLen (219 bytes) 211 6 java.math.BigInteger::addOne (77 bytes) 215 7 java.math.BigInteger::squareToLen (172 bytes) 219 8 java.math.BigInteger::primitiveLeftShift (79 bytes) 224 9 java.math.BigInteger::montReduce (99 bytes) 244 10 sun.security.provider.SHA::implCompress (491 bytes) 280 11 sun.nio.cs.UTF_8$Encoder::encodeArrayLoop (490 bytes) 282 12 java.lang.String::equals (88 bytes) 11400 289 13 java.lang.String::indexOf (151 bytes) 293 14 java.io.UnixFileSystem::normalize (75 bytes) 298 15 java.lang.Object::<init> (1 bytes) 298 16 java.util.jar.Manifest$FastInputStream::readLine (167 bytes) 299 17 java.lang.CharacterDataLatin1::getProperties (11 bytes) 300 18 NormalState::triangleFract (74 bytes) 308 19 java.math.BigInteger::add (178 bytes) 336 20 java.lang.String::lastIndexOf (151 bytes) 337 21 java.lang.Number::<init> (5 bytes) 338 22 java.lang.Character::digit (6 bytes) 340 23 java.lang.Character::digit (168 bytes) 342 24 java.lang.CharacterDataLatin1::digit (85 bytes) 343 25 java.math.BigInteger::trustedStripLeadingZeroInts (37 bytes) 357 26 java.lang.String::substring (83 bytes) 360 27 java.lang.String::lastIndexOf (10 bytes) 360 28 java.lang.String::lastIndexOf (29 bytes) 361 29 java.math.BigInteger::<init> (24 bytes) 361 30 java.lang.Integer::parseInt (269 bytes) 361 31 java.math.BigInteger::<init> (8 bytes) 362 32 java.math.BigInteger::<init> (347 bytes) 404 33 java.math.BigInteger::multiply (72 bytes) 404 34 java.math.BigInteger::add (123 bytes) 

May it be compilation? 可以编译吗? Let's try to delay compilation so it affects us as late as possible. 让我们尝试延迟编译,以便尽可能晚地影响我们。 I tried to play with -XX:CompileThreshold flag and pretty soon found a value ( -XX:CompileThreshold=1000000 ) which doesn't let my loop jump over 11500 . 我尝试使用-XX:CompileThreshold标志并很快找到一个值( -XX:CompileThreshold=1000000 ),这不会让我的循环超过11500

UPDATE UPDATE

I finally reproduced it without any tweaking of compilation threshold. 我终于在没有任何编译阈值调整的情况下重现了它。 For me it looks like it only happens when I run program in my IDE (IntelliJ IDEA). 对我来说,它似乎只有在我的IDE(IntelliJ IDEA)中运行程序时才会发生。 So it may have something to do with IDEA's launcher. 所以它可能与IDEA的发射器有关。 I copied its command line and used it in a small script: 我复制了它的命令行并在一个小脚本中使用它:

for I in `seq 1 100`; do 
        java ... com.intellij.rt.execution.application.AppMain \
        Triangle 2>&1| grep Stack; done | wc -l

And what I found out it usually prints out something less than 100 (95-98). 而我发现它通常打印出小于100(95-98)的东西。 Which is consistent to what I saw when did it manually. 这与我手动操作时所看到的一致。 When I skip the launcher: 当我跳过启动器时:

for I in `seq 1 100`; do 
        java \
        Triangle 2>&1| grep Stack; done | wc -l

it always prints out 100. 它总是打印出100。

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

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