我有一个简单的方法,我为类作业分配编写,使用递归(是的,它必须使用递归)来计算分形图案中的三角形数量:
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"));
}
}
我一直在努力做的是了解int层的大小,以限制用户输入。经过一些测试后,我得到了大约6700+的堆栈溢出,这很好。
令我感到不安的是,如果图层数以千计,则该方法通常会运行,但它仍然可以随机遇到StackOverflowError
。
例如,我选择将图层限制为4444,并且它似乎能够处理它几乎总是,但每隔一段时间它似乎仍然会溢出。
为什么这样做?我能做些什么吗?
答案 0 :(得分:2)
考虑转向迭代版本。我认为,如果您开发递归算法,必须控制水平深度,或者根本不使用递归。
答案 1 :(得分:2)
也许JVM已经确定(通过转义分析)可以在堆栈而不是堆上分配BigInteger。根据实现此优化的时间,所需的堆栈大小会有所不同。
尽管如此,可能还有很多其他原因,而且行为很可能取决于您使用的JVM。
答案 2 :(得分:0)
允许递归 深度是一种设计气味。
试试这个迭代版本:
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;
}
注意:
BigInteger.ZERO
和BigInteger.ONE
代替为这些值创建新实例else
- 在终止语句后 没有else
(例如return
)new BigInteger("2")
和new BigInteger("3")
,而不是每次迭代创建新实例答案 3 :(得分:0)
实际上你可以做些什么:增加最大堆栈大小。这是在JVM启动时使用选项-Xss
完成的,如下所示:
java -Xss40m MainClass
小心不要设置过高的值。如果你必须超过60M - 70M,那么建议重新设计你的代码。
答案 4 :(得分:0)
我无法重现你的“波动”效应。这是非常确定的代码,因此每次都应该得到相同的结果(包括堆栈溢出错误)。
你是如何测试的?你为4444测试的每次尝试都运行了一个新的jvm吗? (或者它只是triangleFrac(4444);在循环中调用?)。
你的操作系统,java版本等等......
我在问,因为我真的不喜欢像这样的未解决的问题 - 这样的事情可能会让你感到伤害的地方(以及何时);。
哦...顺便说一下,尽管它值得,但你应该使用BigInteger中的ONE和ZERO常量(并创建你自己的2和3,就此而言。这应该可以为你节省相当多的内存(是的) ,我知道,这不是你的问题。)答案 5 :(得分:0)
对于那些无法重现这种波动的人。找到layer
值,从哪个方法可靠地抛出StackOverflowError
开始。该值越接近真实阈值越好。现在从循环内部(在我的机器maxLayer = 11500
上)调用此方法:
int i = 11500;
while (true) {
System.out.println(i);
triangleFract(i++);
}
它将抛出StackOverflowError
。现在稍微降低这个值(看起来像5-10%就足够了):
int i = 10500;
while (true) {
System.out.println(i);
triangleFract(i++);
}
在我的机器上,这不会引发任何错误并成功跳过11500
。实际上直到16000
才能正常工作。
无论它是什么,它可能涉及JVM优化。我尝试使用-XX:+PrintCompilation
运行程序。我看到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)
可能是汇编吗?
让我们尝试延迟编译,以便尽可能晚地影响我们。我尝试使用-XX:CompileThreshold
标记,很快就找到了一个值(-XX:CompileThreshold=1000000
),它不会让我的循环跳过11500
。
<强>更新强>
我终于在没有任何编译阈值调整的情况下重现了它。对我来说,它似乎只有在我的IDE(IntelliJ IDEA)中运行程序时才会发生。所以它可能与IDEA的发射器有关。我复制了它的命令行并在一个小脚本中使用它:
for I in `seq 1 100`; do
java ... com.intellij.rt.execution.application.AppMain \
Triangle 2>&1| grep Stack; done | wc -l
而我发现它通常打印出小于100(95-98)的东西。这与我手动操作时所看到的一致。当我跳过发射器时:
for I in `seq 1 100`; do
java \
Triangle 2>&1| grep Stack; done | wc -l
它总是打印出100。