简体   繁体   English

为什么这个方法打印4?

[英]Why does this method print 4?

I was wondering what happens when you try to catch an StackOverflowError and came up with the following method: 我想知道当你试图捕获StackOverflowError时会发生什么,并提出以下方法:

class RandomNumberGenerator {

    static int cnt = 0;

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

Now my question: 现在我的问题:

Why does this method print '4'? 为什么这个方法打印'4'?

I thought maybe it was because System.out.println() needs 3 segments on the call stack, but I don't know where the number 3 comes from. 我想也许是因为System.out.println()在调用堆栈上需要3个段,但我不知道3号来自哪里。 When you look at the source code (and bytecode) of System.out.println() , it normally would lead to far more method invocations than 3 (so 3 segments on the call stack would not be sufficient). 当你查看System.out.println()的源代码(和字节码)时,它通常会导致比3更多的方法调用(因此调用堆栈上的3个段是不够的)。 If it's because of optimizations the Hotspot VM applies (method inlining), I wonder if the result would be different on another VM. 如果是因为优化热点VM应用(方法内联),我想知道其他VM上的结果是否会有所不同。

Edit : 编辑

As the output seems to be highly JVM specific, I get the result 4 using 由于输出似乎是高度JVM特定的,我得到结果4使用
Java(TM) SE Runtime Environment (build 1.6.0_41-b02) Java(TM)SE运行时环境(版本1.6.0_41-b02)
Java HotSpot(TM) 64-Bit Server VM (build 20.14-b01, mixed mode) Java HotSpot(TM)64位服务器VM(内置20.14-b01,混合模式)


Explanation why I think this question is different from Understanding the Java stack : 解释为什么我认为这个问题与理解Java堆栈不同

My question is not about why there is a cnt > 0 (obviously because System.out.println() requires stack size and throws another StackOverflowError before something gets printed), but why it has the particular value of 4, respectively 0,3,8,55 or something else on other systems. 我的问题不是为什么有一个cnt> 0(显然是因为System.out.println()需要堆栈大小并在打印东西之前抛出另一个StackOverflowError ),但为什么它的特定值为4,分别为0,3, 8,55或其他系统上的其他东西。

I think the others have done a good job at explaining why cnt > 0, but there's not enough details regarding why cnt = 4, and why cnt varies so widely among different settings. 我认为其他人已经很好地解释了为什么cnt> 0,但是关于为什么cnt = 4没有足够的细节,以及为什么cnt在不同的设置中变化如此之大。 I will attempt to fill that void here. 我会在这里填补这个空白。

Let

  • X be the total stack size X是总堆栈大小
  • M be the stack space used when we enter main the first time M是我们第一次进入main时使用的堆栈空间
  • R be the stack space increase each time we enter into main R是每次进入main时堆栈空间增加
  • P be the stack space necessary to run System.out.println P是运行System.out.println所需的堆栈空间

When we first get into main, the space left over is XM. 当我们第一次进入main时,留下的空间是XM。 Each recursive call takes up R more memory. 每个递归调用占用R更多内存。 So for 1 recursive call (1 more than original), the memory use is M + R. Suppose that StackOverflowError is thrown after C successful recursive calls, that is, M + C * R <= X and M + C * (R + 1) > X. At the time of the first StackOverflowError, there's X - M - C * R memory left. 因此对于1个递归调用(比原始调用多1个),内存使用是M + R.假设在C成功递归调用之后抛出StackOverflowError,即M + C * R <= X和M + C *(R + 1)> X.在第一个StackOverflowError时,剩下X-M-C * R内存。

To be able to run System.out.prinln , we need P amount of space left on the stack. 为了能够运行System.out.prinln ,我们需要在堆栈上留下P空间。 If it so happens that X - M - C * R >= P, then 0 will be printed. 如果发生X-M-C * R> = P,那么将打印0。 If P requires more space, then we remove frames from the stack, gaining R memory at the cost of cnt++. 如果P需要更多空间,那么我们从堆栈中删除帧,以cnt ++为代价获得R内存。

When println is finally able to run, X - M - (C - cnt) * R >= P. So if P is large for a particular system, then cnt will be large. println最终能够运行时,X - M - (C - cnt)* R> = P.因此,如果P对于特定系统来说很大,则cnt将很大。

Let's look at this with some examples. 让我们用一些例子来看看这个。

Example 1: Suppose 例1:假设

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

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

Example 2: Suppose that 例2:假设

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

Then C = 19, and cnt = 2. 然后C = 19,cnt = 2。

Example 3: Suppose that 例3:假设

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

Then C = 20, and cnt = 3. 然后C = 20,cnt = 3。

Example 4: Suppose that 例4:假设

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

Then C = 19, and cnt = 2. 然后C = 19,cnt = 2。

Thus, we see that both the system (M, R, and P) and the stack size (X) affects cnt. 因此,我们看到系统(M,R和P)和堆栈大小(X)都影响cnt。

As a side note, it does not matter how much space catch requires to start. 作为旁注,开始需要多少空间catch并不重要。 As long as there is not enough space for catch , then cnt will not increase, so there are no external effects. 只要没有足够的空间来catch ,那么cnt就不会增加,所以没有外部效应。

EDIT 编辑

I take back what I said about catch . 我收回了我所说的关于catch It does play a role. 它确实发挥了作用。 Suppose it requires T amount of space to start. 假设它需要T空间来启动。 cnt starts to increment when the leftover space is greater than T, and println runs when the leftover space is greater than T + P. This adds an extra step to the calculations and further muddies up the already muddy analysis. 当剩余空间大于T时,cnt开始递增,当剩余空间大于T + P时, println运行。这为计算增加了一个额外的步骤,并进一步混淆了已经泥泞的分析。

EDIT 编辑

I finally found time to run some experiments to back up my theory. 我终于抽出时间进行一些实验来支持我的理论。 Unfortunately, the theory doesn't seem to match up with the experiments. 不幸的是,该理论似乎与实验不符。 What actually happens is very different. 实际发生的情况非常不同。

Experiment setup: Ubuntu 12.04 server with default java and default-jdk. 实验设置:Ubuntu 12.04服务器,默认为java和default-jdk。 Xss starting at 70,000 at 1 byte increments to 460,000. Xss从70,000开始,以1字节为增量,达到460,000。

The results are available at: https://www.google.com/fusiontables/DataSource?docid=1xkJhd4s8biLghe6gZbcfUs3vT5MpS_OnscjWDbM I've created another version where every repeated data point is removed. 结果可从以下网址获得: https//www.google.com/fusiontables/DataSource?docid = 1xkJhd4s8biLghe6gZccfUs3vT5MpS_OnscjWDbM我创建了另一个版本,其中删除了每个重复的数据点。 In other words, only points that are different from the previous are shown. 换句话说,仅显示与先前不同的点。 This makes it easier to see anomalies. 这样可以更容易地看到异常。 https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA https://www.google.com/fusiontables/DataSource?docid=1XG_SRzrrNasepwZoNHqEAKuZlHiAm9vbEdwfsUA

This is the victim of bad recursive call. 这是糟糕的递归调用的受害者。 As you are wondering why the value of cnt varies, it is because the stack size depends on the platform. 当您想知道为什么cnt的值变化时,这是因为堆栈大小取决于平台。 Java SE 6 on Windows has a default stack size of 320k in the 32-bit VM and 1024k in the 64-bit VM. Windows上的Java SE 6在32位VM中的默认堆栈大小为320k,在64位VM中的默认堆栈大小为1024k。 You can read more here . 你可以在这里阅读更多。

You can run using different stack sizes and you will see different values of cnt before the stack overflows- 你可以使用不同的堆栈大小运行,在堆栈溢出之前你会看到不同的cnt值 -

java -Xss1024k RandomNumberGenerator java -Xss1024k RandomNumberGenerator

You don't see the value of cnt being printed multiple times even though the value is greater than 1 sometimes because your print statement is also throwing error which you can debug to be sure through Eclipse or other IDEs. 您没有看到cnt的值被多次打印,即使该值大于1,因为您的print语句也会抛出错误,您可以通过Eclipse或其他IDE调试该错误。

You can change the code to the following to debug per statement execution if you'd prefer- 如果您愿意,可以将代码更改为以下代码以调试每个语句的执行情况 -

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: 更新:

As this getting a lot more attention, let's have another example to make things clearer- 随着这一点得到更多的关注,让我们有另一个例子来让事情变得更加清晰 -

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);

}

We created another method named overflow to do a bad recursion and removed the println statement from the catch block so it doesn't start throwing another set of errors while trying to print. 我们创建了另一个名为overflow的方法来执行错误的递归,并从catch块中删除了println语句,因此在尝试打印时它不会开始抛出另一组错误。 This works as expected. 这按预期工作。 You can try putting System.out.println(cnt); 你可以试试把System.out.println(cnt); statement after cnt++ above and compile. cnt ++之后的语句和编译。 Then run multiple times. 然后多次运行。 Depending on your platform, you may get different values of cnt . 根据您的平台,您可能会获得不同的cnt值。

This is why generally we do not catch errors because mystery in code is not fantasy. 这就是为什么我们通常不会发现错误,因为代码中的神秘不是幻想。

The behavior is dependent upon the stack size (which can be manually set using Xss . The stack size is architecture specific. From JDK 7 source code : 行为取决于堆栈大小(可以使用Xss手动设置。堆栈大小是特定于体系结构的。来自JDK 7 源代码

// Default stack size on Windows is determined by the executable (java.exe // Windows上的默认堆栈大小由可执行文件确定(java.exe
// has a default value of 320K/1MB [32bit/64bit]). //的默认值为320K / 1MB [32bit / 64bit])。 Depending on Windows version, changing 根据Windows版本,更改
// ThreadStackSize to non-zero may have significant impact on memory usage. // ThreadStackSize为非零可能会对内存使用产生重大影响。
// See comments in os_windows.cpp. //请参阅os_windows.cpp中的注释。

So when the StackOverflowError is thrown, the error is caught in catch block. 因此,当抛出StackOverflowError时,错误会在catch块中捕获。 Here println() is another stack call which throws exception again. 这里println()是另一个再次抛出异常的堆栈调用。 This gets repeated. 这会重复出现。

How many times it repeates? 重复多少次? - Well it depends on when JVM thinks it is no longer stackoverflow. - 这取决于JVM何时认为它不再是stackoverflow。 And that depends on the stack size of each function call (difficult to find) and the Xss . 这取决于每个函数调用(难以找到)和Xss的堆栈大小。 As mentioned above default total size and size of each function call (depends on memory page size etc) is platform specific. 如上所述,每个函数调用的默认总大小和大小(取决于内存页面大小等)是特定于平台的。 Hence different behavior. 因此不同的行为。

Calling the java call with -Xss 4M gives me 41 . -Xss 4M调用java调用给了我41 Hence the correlataion. 因此相关性。

I think the number displayed is the number of time the System.out.println call throws the Stackoverflow exception. 我认为显示的数字是System.out.println调用抛出Stackoverflow异常的时间。

It probably depend on the implementation of the println and the number of stacking call it is made in it. 它可能取决于println的实现以及在其中进行的堆叠调用的次数。

As an illustration: 作为说明:

The main() call trigger the Stackoverflow exception at call i. main()调用在调用i时触发Stackoverflow异常。 The i-1 call of main catch the exception and call println which trigger a second Stackoverflow . main的i-1调用捕获异常并调用println ,触发第二个Stackoverflow cnt get increment to 1. The i-2 call of main catch now the exception and call println . cnt得到增量为1.主要捕获的i-2调用现在是异常并调用println In println a method is called triggering a 3rd exception. println一种方法称为触发第三种异常。 cnt get increment to 2. this continue until println can make all its needed call and finally display the value of cnt . cnt得到增量为2.这继续直到println可以进行所有需要的调用并最终显示cnt的值。

This is then dependent of the actual implementation of println . 这取决于println的实际实现。

For the JDK7 either it detect cycling call and throws the exception earlier either it keep some stack resource and throw the exception before reaching the limit to give some room for remediation logic either the println implementation doesn't make calls either the ++ operation is done after the println call thus is by pass by the exception. 对于JDK7,它要么检测循环调用并且在更早之前抛出异常,要么保留一些堆栈资源并在达到限制之前抛出异常以给补救逻辑留出一些空间,否则println实现不会调用++操作完成因此, println调用之后通过异常。

  1. main recurses on itself until it overflows the stack at recursion depth R . main递归直到它在递归深度R溢出堆栈。
  2. The catch block at recursion depth R-1 is run. 运行递归深度为R-1的catch块。
  3. The catch block at recursion depth R-1 evaluates cnt++ . 递归深度为R-1的catch块评估cnt++
  4. The catch block at depth R-1 calls println , placing cnt 's old value on the stack. 深度为R-1的catch块调用println ,将cnt的旧值放在堆栈上。 println will internally call other methods and uses local variables and things. println将在内部调用其他方法并使用局部变量和事物。 All these processes require stack space. 所有这些过程都需要堆栈空间。
  5. Because the stack was already grazing the limit, and calling/executing println requires stack space, a new stack overflow is triggered at depth R-1 instead of depth R . 因为堆栈已经在放大限制,并且调用/执行println需要堆栈空间,所以在深度R-1而不是深度R处触发新的堆栈溢出。
  6. Steps 2-5 happen again, but at recursion depth R-2 . 步骤2-5再次发生,但是在递归深度R-2
  7. Steps 2-5 happen again, but at recursion depth R-3 . 步骤2-5再次发生,但是在递归深度R-3
  8. Steps 2-5 happen again, but at recursion depth R-4 . 步骤2-5再次发生,但是在递归深度R-4
  9. Steps 2-4 happen again, but at recursion depth R-5 . 步骤2-4再次发生,但是在递归深度R-5
  10. It so happens that there is enough stack space now for println to complete (note that this is an implementation detail, it may vary). 碰巧现在有足够的堆栈空间供println完成(注意这是一个实现细节,它可能会有所不同)。
  11. cnt was post-incremented at depths R-1 , R-2 , R-3 , R-4 , and finally at R-5 . cnt在深度R-1R-2R-3R-4后递增,最后在R-5处递增。 The fifth post-increment returned four, which is what was printed. 第五个后增量返回四,这是打印的。
  12. With main completed successfully at depth R-5 , the whole stack unwinds without more catch blocks being run and the program completes. mainR-5深度成功完成,整个堆栈展开,没有更多的catch块运行并且程序完成。

After digging around for a while, I can't say that I find the answer, but I think it's quite close now. 经过一段时间的挖掘后,我不能说我找到了答案,但我认为现在已经很接近了。

First, we need to know when a StackOverflowError will be thrown. 首先,我们需要知道何时抛出StackOverflowError In fact, the stack for a java thread stores frames, which containing all the data needed for invoking a method and resume. 实际上,java线程的堆栈存储了框架,其中包含调用方法和恢复所需的所有数据。 According to Java Language Specifications for JAVA 6 , when invoking a method, 根据JAVA 6的Java语言规范 ,在调用方法时,

If there is not sufficient memory available to create such an activation frame, an StackOverflowError is thrown. 如果没有足够的可用内存来创建这样的激活帧,则抛出StackOverflowError。

Second, we should make it clear what is " there is not sufficient memory available to create such an activation frame ". 其次,我们应该明确“ 没有足够的内存来创建这样的激活框架 ”。 According to Java Virtual Machine Specifications for JAVA 6 , 根据JAVA 6的Java虚拟机规范

frames may be heap allocated. 帧可以是堆分配的。

So, when a frame is created, there should be enough heap space to create a stack frame and enough stack space to store the new reference which point to the new stack frame if the frame is heap allocated. 因此,当创建一个框架时,应该有足够的堆空间来创建一个堆栈框架和足够的堆栈空间来存储新的引用,如果框架是堆分配的,则该引用指向新的堆栈框架。

Now let's go back to the question. 现在让我们回到这个问题。 From the above, we can know that when a method is execute, it may just costs the same amount of stack space. 从上面我们可以知道,当一个方法执行时,它可能只需要相同数量的堆栈空间。 And invoking System.out.println (may) needs 5 level of method invocation, so 5 frames need to be created. 并且调用System.out.println (可能)需要5级方法调用,因此需要创建5个帧。 Then when StackOverflowError is thrown out, it has to go back 5 times to get enough stack space to store 5 frames' references. 然后,当抛出StackOverflowError时,它必须返回5次以获得足够的堆栈空间来存储5帧的引用。 Hence 4 is print out. 因此打印出4。 Why not 5? 为什么不5? Because you use cnt++ . 因为你使用cnt++ Change it to ++cnt , and then you will get 5. 将其更改为++cnt ,然后您将获得5。

And you will notice that when the size of stack go to a high level, you will get 50 sometimes. 而且你会注意到当堆栈的大小达到很高的水平时,你有时会得到50。 That is because the amount of available heap space need to be taken into consideration then. 那是因为需要考虑可用堆空间的数量。 When the stack's size is too large, maybe heap space will run out before stack. 当堆栈的大小太大时,堆栈空间可能会在堆栈之前耗尽。 And (maybe) the actual size of stack frames of System.out.println is about 51 times of main , therefore it goes back 51 times and print 50. 并且(可能) System.out.println的堆栈帧的实际大小约为main 51倍,因此它返回51次并打印50。

This is not exactly an answer to the question but I just wanted to add something to the original question that I came across and how I understood the problem: 这不是问题的答案,但我只想在我遇到的原始问题中添加一些内容以及我如何理解问题:

In the original problem the exception is caught where it was possible: 在最初的问题中,异常会被捕获到可能的位置:

For example with jdk 1.7 it is caught at first place of occurence. 例如,对于jdk 1.7,它会在出现的第一个位置被捕获。

but in earlier versions of jdk it looks like the exception is not being caught at the first place of occurence hence 4, 50 etc.. 但是在早期版本的jdk中,看起来异常并没有在出现的第一个位置被捕获,因此4,50等。

Now if you remove the try catch block as following 现在,如果您删除try catch块,如下所示

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

Then you will see all the values of cnt ant the thrown exceptions (on jdk 1.7). 然后你会看到cnt所有值蚂蚁抛出异常(在jdk 1.7上)。

I used netbeans to see the output, as the cmd will not show all the output and exception thrown. 我使用netbeans来查看输出,因为cmd不会显示所有输出和异常抛出。

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

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