Java提供了看似随机的StackOverflowError

时间:2018-08-19 19:50:19

标签: java

这是我的一个项目内部的阶乘函数:

public class Fac {
    public static void main(String[] args) {
        System.out.println(fac(9000));
    }

    public static long fac(long x) {
        if (x <= 1) {
            return 1;
        }
        else {
            return x * fac(x-1);
        }
    }
}

当此函数以较大的参数运行时,例如fac(9000),有时但并非总是会导致StackOverflowError。我注意到,当我从IDE内部启动调用fac(9000)的程序(IntelliJ Community Edition 2018.2.1)时,错误发生的次数大约是三分之二,但是当我从外壳启动时,可以被调用超过300次或更多,而崩溃仅一次或两次(我使用了简短的bash脚本对此进行了测试)。我在Ubuntu 18.04上使用openjdk-11。

我尝试使用-Xss1024k设置显式堆栈大小(也使用了其他一些大小),但是问题仍然存在。

我知道存储在long中的结果将不再正确(当它不崩溃时,结果为0),但是我不知道是什么原因导致了看似随机的崩溃以及为什么从IDE内部运行时,发生这种情况的频率更高。

1 个答案:

答案 0 :(得分:0)

几乎可以肯定这是一个准时制问题。

值得注意的是,由于Java中的“堆栈”不是很常规,因此StackOverflowError的触发方式很奇怪。我不会太深入细节,但是它本质上似乎是堆中的链表。

鉴于人们在评论中报告了一些症状,我认为这与JIT密切相关。引用西蒙

  

当我以简单的for(int i = 0; i <100000; ++ i)调用该方法时{fib(9000); }出现以下两种情况之一:程序在第一次调用fib(9000)时崩溃,或者在100000次调用中都没有崩溃。

通常,应该在一致的点激活JIT,尤其是在运行中没有差异的情况下。如果您决定打开编译打印,则会注意到正在编译的许多类似乎根本与您的程序无关。从某种意义上来说,这可能是有问题的,因为您的代码可能需要一段时间才能被编译(JIT编译作为队列处理)。另一个问题是JIT是非阻塞的。这意味着将触发JIT,然后将其编译到另一个线程中,然后将方法指针注入程序中。因为它是非阻塞的,所以它可能在函数触发StackOverflowError之后结束。

有很多方法可以提高成功几率。

  1. 通过多次调用该方法来预热该方法,该数字不会触发StackOverflowError。这应该给JIT时间注入已编译的方法。
  2. 设置程序以单核模式运行。通过打开任务管理器,右键单击Java进程,然后单击Go to details,可以完成此操作(我只能在Win10中确认如何执行此操作)。这将带您选择过程的“详细信息”页面。现在,您可以右键单击流程详细信息,然后选择Set affinity,取消选择除一个之外的所有内核。这将强制程序在单个核心上下文中运行,这将导致所有线程(包括JIT)被阻塞。确实有一个值得注意的警告,即GC现在将被阻塞。
  3. 按照与之前相同的步骤进入“详细信息”页面,右键单击流程详细信息并选择Set priority,然后选择High(我发现选择Realtime可能会停滞系统)。这样一来,进程线程将在列表中排在首位,以运行其他事情,这有望使您的JIT在内核上获得更多连续时间。
  4. 将程序放在最小的linux服务器上,就像#3一样。这里的目标是使服务器上没有其他任何运行。确实不建议这样做,但可能可以。

对于#2和#3,在设置优先级或相似性之前,您可能必须在程序中添加一些内容以停止执行。您可以选择使用Process Hacker之类的程序来保存优先级或相似性,并在每次启动时立即进行设置。