为什么这种方法打印4?

2022-08-31 09:36:11

我想知道当你尝试捕获StackOverflowError并提出以下方法时会发生什么:

class RandomNumberGenerator {

    static int cnt = 0;

    public static void main(String[] args) {
        try {
            main(args);
        } catch (StackOverflowError ignore) {
            System.out.println(cnt++);
        }
    }
}

现在我的问题:

为什么此方法打印“4”?

我想也许是因为调用堆栈上需要3个段,但我不知道数字3来自哪里。当您查看 的源代码(和字节码)时,它通常会导致比 3 个更多的方法调用(因此调用堆栈上的 3 个段是不够的)。如果是因为热点VM应用的优化(方法内联),我想知道在另一个VM上的结果是否会有所不同。System.out.println()System.out.println()

编辑

由于输出似乎是高度特定于JVM的,因此我使用
Java(TM)SE运行时环境(构建1.6.0_41-b02)
Java HotSpot(TM)64位服务器VM(构建20.14-b01,混合模式)获得结果4


解释为什么我认为这个问题与理解Java堆栈不同:

我的问题不是为什么有一个cnt>0(显然是因为需要堆栈大小并在打印某些东西之前抛出另一个),而是为什么它的特定值分别为0,3,8,55或其他系统。System.out.println()StackOverflowError


答案 1

我认为其他人在解释为什么cnt>0方面做得很好,但是关于为什么cnt = 4,以及为什么cnt在不同设置之间变化如此之大,没有足够的细节。我将试图填补这一空白。

  • X 为总堆栈大小
  • M 是我们第一次进入 main 时使用的堆栈空间
  • R 是堆栈空间每次我们进入主
  • P 是运行所需的堆栈空间System.out.println

当我们第一次进入main时,剩下的空间是X-M。每个递归调用占用 R 更多的内存。所以对于1个递归调用(比原来的多1个),内存使用是M+R.假设StackOverflowError在C成功递归调用后被抛出,即M + C * R <= X和M + C * (R + 1)>X。在第一个StackOverflowError时,还剩下X - M - C * R内存。

为了能够运行,我们需要在堆栈上留下P的空间。如果发生X - M - C * R>= P的情况,则将打印0。如果 P 需要更多空间,那么我们从堆栈中删除帧,以 cnt++ 为代价获得 R 内存。System.out.prinln

当最终能够运行时,X - M - (C - cnt) * R > = P。因此,如果 P 对于特定系统较大,则 cnt 将很大。println

让我们通过一些示例来看看这一点。

示例 1:假设

  • X = 100
  • M = 1
  • R = 2
  • P = 1

则 C = floor((X-M)/R) = 49,cnt = ceiling((P - (X - M - C*R))/R) = 0。

示例 2:假设

  • X = 100
  • M = 1
  • R = 5
  • P = 12

则 C = 19,cnt = 2。

示例 3:假设

  • X = 101
  • M = 1
  • R = 5
  • P = 12

则 C = 20,cnt = 3。

示例 4:假设

  • X = 101
  • M = 2
  • R = 5
  • P = 12

则 C = 19,cnt = 2。

因此,我们看到系统(M,R和P)和堆栈大小(X)都会影响cnt。

作为旁注,启动需要多少空间并不重要。只要没有足够的空间,那么cnt就不会增加,所以没有外在的影响。catchcatch

编辑

我收回我说过的话。它确实发挥了作用。假设它需要 T 量的空间才能启动。当剩余空间大于 T 时,cnt 开始递增,当剩余空间大于 T + P 时运行。这为计算增加了一个额外的步骤,并进一步混淆了已经混乱的分析。catchprintln

编辑

我终于抽出时间做了一些实验来支持我的理论。不幸的是,该理论似乎与实验不符。实际发生的事情是非常不同的。

实验设置:Ubuntu 12.04服务器,默认java和default-jdk。Xss 从 70,000 开始,以 1 字节为增量到 460,000。

结果可在以下位置获得:https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM 我创建了另一个版本,其中每个重复的数据点都被删除了。换句话说,仅显示与前一点不同的点。这样可以更轻松地查看异常。https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA


答案 2

这是不良递归调用的受害者。当您想知道为什么cnt的值会发生变化时,这是因为堆栈大小取决于平台。Windows 上的 Java SE 6 在 32 位 VM 中的默认堆栈大小为 320k,在 64 位 VM 中的默认堆栈大小为 1024k。您可以在此处阅读更多内容

您可以使用不同的堆栈大小运行,并且在堆栈溢出之前,您将看到不同的 cnt 值 -

java -Xss1024k RandomNumberGenerator

您不会看到 cnt 的值被多次打印,即使该值有时大于 1,因为您的 print 语句也抛出了错误,您可以通过 Eclipse 或其他 IDE 进行调试以确保。

如果您愿意,可以将代码更改为以下内容以调试每个语句的执行 -

static int cnt = 0;

public static void main(String[] args) {                  

    try {     

        main(args);   

    } catch (Throwable ignore) {

        cnt++;

        try { 

            System.out.println(cnt);

        } catch (Throwable t) {   

        }        
    }        
}

更新:

随着这一点得到更多的关注,让我们再举一个例子来使事情变得更清晰 -

static int cnt = 0;

public static void overflow(){

    try {     

      overflow();     

    } catch (Throwable t) {

      cnt++;                      

    }

}

public static void main(String[] args) {

    overflow();
    System.out.println(cnt);

}

我们创建了另一个名为 overflow 的方法来执行错误的递归,并从 catch 块中删除了 println 语句,这样它就不会在尝试打印时开始抛出另一组错误。这按预期工作。你可以尝试把 System.out.println(cnt); 语句放在上面的 cnt++ 之后并进行编译。然后运行多次。根据您的平台,您可能会获得不同的 cnt 值。

這就是為什麼我們通常不會捕捉錯誤,因為代謊中的神秘不是幻想。


推荐