首页 文章

为什么这个方法打印4?

提问于
浏览
112

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

class RandomNumberGenerator {

    static int cnt = 0;

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

现在我的问题:

为什么这个方法打印'4'?

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

Edit

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

Explanation why I think this question is different from Understanding the Java stack:

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

7 回答

  • 0

    我认为其他人已经很好地解释了为什么cnt> 0,但是关于为什么cnt = 4没有足够的细节,以及为什么cnt在不同的设置中变化如此之大 . 我会在这里填补这个空白 .

    • X是总堆栈大小

    • M是我们第一次进入main时使用的堆栈空间

    • R每次进入main时堆栈空间增加

    • P是运行 System.out.println 所需的堆栈空间

    当我们第一次进入主要时,留下的空间是X-M . 每个递归调用占用R更多内存 . 因此,对于1个递归调用(比原始调用多1个),内存使用是M R.假设在C成功递归调用之后抛出StackOverflowError,即MC * R <= X且MC *(R 1)> X.第一个StackOverflowError的时间,剩下X - M - C * R内存 .

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

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

    让我们用一些例子来看看这个 .

    Example 1: 假设

    • X = 100

    • M = 1

    • R = 2

    • P = 1

    然后C = floor((X-M)/ R)= 49,并且cnt = ceiling((P - (X-M-C * R))/ R)= 0 .

    Example 2: 假设

    • X = 100

    • M = 1

    • R = 5

    • P = 12

    然后C = 19,cnt = 2 .

    Example 3: 假设

    • X = 101

    • M = 1

    • R = 5

    • P = 12

    然后C = 20,cnt = 3 .

    Example 4: 假设

    • X = 101

    • M = 2

    • R = 5

    • P = 12

    然后C = 19,cnt = 2 .

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

    作为旁注, catch 需要多少空间才能启动 . 只要 catch 没有足够的空间,cnt就不会增加,所以没有外部效果 .

    EDIT

    我收回了我所说的 catch . 它确实发挥了作用 . 假设它需要T空间来启动 . 当剩余空间大于T时,cnt开始递增,当剩余空间大于T P时,ctt开始运行 . 这为计算增加了额外的步骤,并进一步混淆了已经混浊的分析 .

    EDIT

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

    实验设置: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

  • 1

    这是糟糕的递归调用的受害者 . 当您想知道为什么cnt的值变化时,这是因为堆栈大小取决于平台 . Windows上的Java SE 6在32位VM中的默认堆栈大小为320k,在64位VM中的默认堆栈大小为1024k . 你可以阅读更多here .

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

    java -Xss1024kRandomNumberGenerator

    您没有看到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) {   
    
            }        
        }        
    }
    

    UPDATE:

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

    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值 .

    这就是为什么我们通常不会发现错误,因为代码中的神秘不是幻想 .

  • 6

    行为取决于堆栈大小(可以使用 Xss 手动设置 . 堆栈大小是特定于体系结构的 . 来自JDK 7 source code

    // Windows上的默认堆栈大小由可执行文件确定(java.exe //默认值为320K / 1MB [32bit / 64bit]) . 根据Windows版本,将// ThreadStackSize更改为非零可能会对内存使用产生重大影响 . //请参阅os_windows.cpp中的注释 .

    因此,当抛出 StackOverflowError 时,错误会在catch块中捕获 . 这里 println() 是另一个再次抛出异常的堆栈调用 . 这会重复出现 .

    重复多少次? - 这取决于JVM何时认为它不再是stackoverflow . 这取决于每个函数调用的堆栈大小(很难找到)和 Xss . 如上所述,每个函数调用的默认总大小和大小(取决于内存页面大小等)是特定于平台的 . 因此不同的行为 .

    -Xss 4M 调用 java 调用给了我 41 . 因此相关性 .

  • 20

    我认为显示的数字是 System.out.println 调用抛出 Stackoverflow 异常的时间 .

    它可能取决于 println 的实现以及它在其中进行的堆叠调用次数 .

    作为说明:

    main() 调用在调用i时触发 Stackoverflow 异常 . main的i-1调用捕获异常并调用 println ,触发第二个 Stackoverflow . cnt 得到增量为1.主要捕获的i-2调用现在是异常并调用 println . 在 println 中,一个方法被称为触发第三个异常 . cnt 得到增量到2.这继续直到 println 可以进行所有需要的调用,最后显示 cnt 的值 .

    这取决于 println 的实际实现 .

    对于JDK7,它要么检测循环调用并在之前抛出异常,要么保留一些堆栈资源并在达到限制之前抛出异常以给予补救逻辑一些空间 println 实现不会调用操作或者操作完成后 println 因此通过异常调用 .

  • 41
    • main 自我递归直到它在递归深度 R 溢出堆栈 .

    • 运行递归深度 R-1 处的catch块 .

    • 递归深度 R-1 处的catch块计算 cnt++ .

    • 深度为 R-1 的catch块调用 println ,将 cnt 的旧值放在堆栈上 . println 将在内部调用其他方法并使用局部变量和事物 . 所有这些过程都需要堆栈空间 .

    • 因为堆栈已经放弃了限制,并且调用/执行 println 需要堆栈空间,所以在深度 R-1 而不是深度 R 处触发新的堆栈溢出 .

    • 步骤2-5再次发生,但在递归深度 R-2 .

    • 步骤2-5再次发生,但在递归深度 R-3 .

    • 步骤2-5再次发生,但在递归深度 R-4 .

    • 步骤2-4再次发生,但在递归深度 R-5 .

    • 碰巧现在有足够的堆栈空间来完成 println (注意这是一个实现细节,它可能会有所不同) .

    • cnt 在深度 R-1R-2R-3R-4 后递增,最后在 R-5 . 第五个后增量返回四,这是打印的 .

    • 随着 main 在深度 R-5 成功完成,整个堆栈展开而不会运行更多的catch块并且程序完成 .

  • 6

    经过一段时间的挖掘后,我不能说我找到了答案,但我认为现在已经很接近了 .

    首先,我们需要知道何时抛出 StackOverflowError . 实际上,java线程的堆栈存储了框架,其中包含调用方法和恢复所需的所有数据 . 根据Java Language Specifications for JAVA 6,在调用方法时,

    如果没有足够的可用内存来创建此类激活帧,则抛出StackOverflowError .

    其次,我们应该明确“没有足够的内存来创建这样的激活框架” . 根据Java Virtual Machine Specifications for JAVA 6

    帧可能是堆分配的 .

    因此,当创建一个框架时,应该有足够的堆空间来创建一个堆栈框架和足够的堆栈空间来存储新的引用,如果框架是堆分配的,则该引用指向新的堆栈框架 .

    现在让我们回到这个问题 . 从上面我们可以知道,当一个方法执行时,它可能只需要相同数量的堆栈空间 . 并且调用 System.out.println (可能)需要5级方法调用,因此需要创建5个帧 . 然后当 StackOverflowError 被抛出时,它必须返回5次以获得足够的堆栈空间来存储5帧的引用 . 因此打印出4 . 为什么不5?因为你使用 cnt++ . 将其更改为 ++cnt ,然后您将获得5 .

    而且你会注意到当堆栈的大小达到很高的水平时,你有时会得到50 . 那是因为需要考虑可用堆空间的数量 . 当堆栈的大小太大时,堆栈空间可能会在堆栈之前耗尽 . 并且(可能) System.out.println 的堆栈帧的实际大小约为 main 的51倍,因此它返回51次并打印50 .

  • 13

    这不是问题的答案,但我只想在我遇到的原始问题中添加一些内容以及我如何理解问题:

    在最初的问题中,异常会被捕获到可能的位置:

    例如,对于jdk 1.7,它会在出现的第一个位置被捕获 .

    但是在早期版本的jdk中,看起来异常并没有在出现的第一个位置被捕获,因此4,50等 .

    现在,如果您删除try catch块,如下所示

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

    然后你会看到 cnt 的所有值 Ant 抛出异常(在jdk 1.7上) .

    我使用netbeans来查看输出,因为cmd不会显示所有输出和异常抛出 .

相关问题