111

I was wondering what happens when you try to catch an StackOverflowError and came up with the following method:

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'?

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. 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). If it's because of optimizations the Hotspot VM applies (method inlining), I wonder if the result would be different on another VM.

Edit:

As the output seems to be highly JVM specific, I get the result 4 using
Java(TM) SE Runtime Environment (build 1.6.0_41-b02)
Java HotSpot(TM) 64-Bit Server VM (build 20.14-b01, mixed mode)


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

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.

Community
  • 1
  • 1
flrnb
  • 836
  • 1
  • 7
  • 13
  • 4
    In my local, I am getting "0" as expected. – RaceBase Jul 24 '13 at 08:21
  • ++cnt is giving "1". so the result is as expected. Bit strange from your answer. – RaceBase Jul 24 '13 at 08:23
  • I have run your code and it prints '1' – njzk2 Jul 24 '13 at 08:23
  • For me also it prints `0` as expected. – me_digvijay Jul 24 '13 at 08:25
  • For me it prints `3` as assumed . – AllTooSir Jul 24 '13 at 08:25
  • 1
    jdk1.5 print 12 ... but don't ask me why – Philipp Sander Jul 24 '13 at 08:26
  • For me it prints `0`. So any answer depends on the environment... – Uwe Plonus Jul 24 '13 at 08:26
  • 2
    This may deal with lot of architecture stuff. So better post your output with the jdk version For me output is 0 on jdk 1.7 – Lokesh Jul 24 '13 at 08:26
  • Put the cnt++ before println. And just print cnt. – Sajal Dutta Jul 24 '13 at 08:31
  • @SajalDutta Tried what you said... Printing 58 or 255 – araknoid Jul 24 '13 at 08:36
  • writing: cnt++; System.out.println( cnt ); in the catch-block yields a result of 5 on my machine – flrnb Jul 24 '13 at 08:38
  • 3
    I've gotten `5`, `6` and `38` with Java 1.7.0_10 – Kon Jul 24 '13 at 09:00
  • 1
    Just to add... If we compile once and run the same java class file multiple times. In jdk 1.7, this results in 0 (even after running again and again) But in jdk 1.6, this results in 4 and sometimes 50. – Lokesh Jul 24 '13 at 09:01
  • 1
    There is another question which is quite similar to this question: http://stackoverflow.com/questions/15083318/understanding-java-stack – Alvin Wong Jul 24 '13 at 11:36
  • 8
    @Elist Won't be same output when you're doing tricks involving underlying architecture ;) – m0skit0 Jul 24 '13 at 12:29
  • As this is my first question on StackOverflow: is there any convention for formatting the source code examples? @syb0rg has very kindly added some whitespace to my code, but the linebreaks right before the curly braces look a little bit weird to me :-) – flrnb Jul 24 '13 at 17:09
  • 3
    @flrnb It's just a style I use to line up the braces. It makes it easier for me to know where conditions and functions start and end. You can change it if you want, but in my opinion it's more readable this way. – syb0rg Jul 24 '13 at 17:11
  • @flrnb there aren't any official conventions; but most java code has inlined {}'s to match the style in Sun/Oracle's sample code as does most javascript (automatic ; inserting makes it more or less mandatory). Most C# code has {}'s on new lines to match Microsoft's samples. I don't spend enough time in C or C++ tags to know if any one style dominates in them. – Dan Is Fiddling By Firelight Jul 24 '13 at 20:51
  • 2
    [Obligatory (but eerily relevant) xkcd](http://www.xkcd.com/221/) – Jules Jul 30 '13 at 19:51
  • 1
    @atleastthreecharacters that's why I named the class `RandomNumberGenerator` when it printed 4 ;) – flrnb Jul 31 '13 at 22:28

7 Answers7

41

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. I will attempt to fill that void here.

Let

  • X be the total stack size
  • M be the stack space used when we enter main the first time
  • R be the stack space increase each time we enter into main
  • P be the stack space necessary to run System.out.println

When we first get into main, the space left over is X-M. Each recursive call takes up R more memory. 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.

To be able to run System.out.prinln, we need P amount of space left on the stack. If it so happens that X - M - C * R >= P, then 0 will be printed. If P requires more space, then we remove frames from the stack, gaining R memory at the cost of cnt++.

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.

Let's look at this with some examples.

Example 1: Suppose

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

Then C = floor((X-M)/R) = 49, and cnt = ceiling((P - (X - M - C*R))/R) = 0.

Example 2: Suppose that

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

Then C = 19, and cnt = 2.

Example 3: Suppose that

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

Then C = 20, and cnt = 3.

Example 4: Suppose that

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

Then C = 19, and cnt = 2.

Thus, we see that both the system (M, R, and P) and the stack size (X) affects cnt.

As a side note, it does not matter how much space catch requires to start. As long as there is not enough space for catch, then cnt will not increase, so there are no external effects.

EDIT

I take back what I said about catch. It does play a role. Suppose it requires T amount of space to start. 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.

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. Xss starting at 70,000 at 1 byte increments to 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. 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

John Tseng
  • 6,262
  • 2
  • 27
  • 35
  • Thank you for the good summary, I guess it all boils down to the question: what impacts M, R and P (since X can be set by the VM-option -Xss)? – flrnb Jul 24 '13 at 14:11
  • @flrnb M, R, and P are system specific. You can't easily change those. I expect them to differ between some releases as well. – John Tseng Jul 24 '13 at 14:15
  • Then, why do I get different results by altering Xss (aka X)? Changing X from 100 to 10000, given that M, R and P stay the same, should not affect cnt according to your formula, or am I mistaken? – flrnb Jul 24 '13 at 14:29
  • @flrnb X alone does change cnt due to the discrete nature of these variables. Examples 2 and 3 only differ on X, but cnt is different. – John Tseng Jul 24 '13 at 14:45
  • How can two cnt for two different X values differ by more than 1? – flrnb Jul 24 '13 at 14:56
  • followup: they actually differ only by one for different -Xss values in most cases. However, -Xss4M prints 4, whereas -Xss6M prints 50 on my machine, but this seems to be a corner case – flrnb Jul 24 '13 at 15:09
  • I think you're selling yourself short. This is a much better explanation than the higher voted answers – Richard Tingle Jul 24 '13 at 18:49
  • @flrnb You bring up a good point. I don't understand why cnt is sometimes much larger. I am not sure I buy Jay's answer to that question either. It looks like there's something else at play here. – John Tseng Jul 24 '13 at 20:29
  • @RichardTingle I agree. ;) However, I don't want to repeat the parts of the solution that others have already put forth. How would you suggest that I change my answer to be better? – John Tseng Jul 24 '13 at 20:31
  • 1
    @JohnTseng I also consider your answer the most comprehensible and complete by now - anyway, I would be really interested in what the stack actually _looks_ like the moment the `StackOverflowError` is thrown and how this does affect the output. If it contained only a reference to a stack frame on the heap (as Jay suggested), then the output should be quite predictable for a given system. – flrnb Jul 24 '13 at 21:30
  • @flrnb If the stack frames are on the heap, and we are running out of heap space, then -Xss shouldn't affect the outcome beyond a certain point. I would be curious to see how Xss affects cnt in byte sized increments, e.g. 2000B, 2001B, 2002B, ... 3000B. I think 1000B should be a large enough range to see some interesting affects. Unfortunately, I don't have time today to try it out. I'll try it out later. – John Tseng Jul 24 '13 at 21:41
  • @flrnb I haven't gotten around to trying this out, yet. Have you figured out why cnt fluctuates so much? – John Tseng Jul 26 '13 at 14:45
  • @JohnTseng me neither. But I'm afraid we won't get any closer than "the amount of memory a certain method call requires on the call stack is specific to the system and VM implementation". I therefore accepted your answer, because it shows how these specifics lead to different values getting printed. I guess the details of stack memory allocation should be subject to another question. – flrnb Jul 29 '13 at 07:36
  • @flrnb I just got around to try those experiments. I've posted the data. Unfortunately, the actual behavior is very different than what I had thought. – John Tseng Jul 30 '13 at 15:19
  • @JohnTseng I think if we could get stack size programatically or add dynamically, we could come up with a concrete output based from your logic. But unfortunately none of them is possible. – Sajal Dutta Jul 31 '13 at 10:30
  • @SajalDutta Do yo mean from inside the java program? I can pass it the xss value so that it knows what it is. Or we can do this at the thread level:http://docs.oracle.com/javase/6/docs/api/java/lang/Thread.html I don't think the thread can change its own stack size during runtime, though, only a new thread's stack. – John Tseng Jul 31 '13 at 13:46
  • @JohnTseng Yes inside. You cannot do any changes on stack size dynamically which is unfortunate. – Sajal Dutta Jul 31 '13 at 13:54
  • @SajalDutta What would you do if a thread *could* change its own stack size? – John Tseng Jul 31 '13 at 13:59
  • @JohnTseng Say you could get the stacksize and change it too - in that case at start you get the size. When stack goes to overflow, you check the size and do a diff. You will have an estimate. Now, you can do the same for you to know how much a print cost. When it overflows and we increase the size by the estimate we got for a print, it would be able to print and will gracefully exit. – Sajal Dutta Jul 31 '13 at 14:19
19

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. Java SE 6 on Windows has a default stack size of 320k in the 32-bit VM and 1024k in the 64-bit VM. You can read more here.

You can run using different stack sizes and you will see different values of cnt before the stack overflows-

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.

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. This works as expected. You can try putting System.out.println(cnt); statement after cnt++ above and compile. Then run multiple times. Depending on your platform, you may get different values of cnt.

This is why generally we do not catch errors because mystery in code is not fantasy.

Sajal Dutta
  • 18,272
  • 11
  • 52
  • 74
13

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:

// Default stack size on Windows is determined by the executable (java.exe
// has a default value of 320K/1MB [32bit/64bit]). Depending on Windows version, changing
// ThreadStackSize to non-zero may have significant impact on memory usage.
// See comments in os_windows.cpp.

So when the StackOverflowError is thrown, the error is caught in catch block. Here println() is another stack call which throws exception again. This gets repeated.

How many times it repeates? - Well it depends on when JVM thinks it is no longer stackoverflow. And that depends on the stack size of each function call (difficult to find) and the 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. Hence the correlataion.

Community
  • 1
  • 1
Jatin
  • 31,116
  • 15
  • 98
  • 163
  • 4
    I don't get why the stack size should impact the result, since it is already exceeded when we try to print the value of cnt. Thus, the only difference could come from the "stack size of each function call". And I don't understand why this should vary among 2 machines running the same JVM version. – flrnb Jul 24 '13 at 12:07
  • The exact behavior can only be obtained from the JVM source. But the reason could be this. Remember even `catch` is a block and that occupies memory on stack. How much memory does each method call takes cannot be known. When the stack gets cleared, you are adding one more block of `catch` and so. This might be the behavior. This is only speculation. – Jatin Jul 24 '13 at 12:10
  • And stack size can differ in two different machines. Stack size depends on a lot of os-based factors namely memory page-size etc – Jatin Jul 24 '13 at 12:13
6

I think the number displayed is the number of time the System.out.println call throws the Stackoverflow exception.

It probably depend on the implementation of the println and the number of stacking call it is made in it.

As an illustration:

The main() call trigger the Stackoverflow exception at call i. The i-1 call of main catch the exception and call println which trigger a second Stackoverflow. cnt get increment to 1. The i-2 call of main catch now the exception and call println. In println a method is called triggering a 3rd exception. cnt get increment to 2. this continue until println can make all its needed call and finally display the value of cnt.

This is then dependent of the actual implementation of 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.

Kazaag
  • 2,115
  • 14
  • 14
  • That's what I meant by "I thought maybe it was because System.out.println needs 3 segments on the call stack" - but I was puzzled why it's exactly this number and now I am even more puzzled why the number differs so largely among different (virtual) machines – flrnb Jul 24 '13 at 11:13
  • I agree partially to it but what I disagree is on the statement ` dependent of the actual implementation of println.` It has got do with stack size in each jvm rather than implementation. – Jatin Jul 24 '13 at 11:51
6
  1. main recurses on itself until it overflows the stack at recursion depth R.
  2. The catch block at recursion depth R-1 is run.
  3. The catch block at recursion depth R-1 evaluates cnt++.
  4. The catch block at depth R-1 calls println, placing cnt's old value on the stack. println will internally call other methods and uses local variables and things. 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.
  6. Steps 2-5 happen again, but at recursion depth R-2.
  7. Steps 2-5 happen again, but at recursion depth R-3.
  8. Steps 2-5 happen again, but at recursion depth R-4.
  9. Steps 2-4 happen again, but at recursion depth 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).
  11. cnt was post-incremented at depths R-1, R-2, R-3, R-4, and finally at 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.
Craig Gidney
  • 17,763
  • 5
  • 68
  • 136
1

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. In fact, the stack for a java thread stores frames, which containing all the data needed for invoking a method and resume. According to Java Language Specifications for JAVA 6, when invoking a method,

If there is not sufficient memory available to create such an activation frame, an StackOverflowError is thrown.

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,

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. Then when StackOverflowError is thrown out, it has to go back 5 times to get enough stack space to store 5 frames' references. Hence 4 is print out. Why not 5? Because you use cnt++. Change it to ++cnt, and then you will get 5.

And you will notice that when the size of stack go to a high level, you will get 50 sometimes. 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.

Jay
  • 1,077
  • 9
  • 9
  • My first thought was also counting the levels of method invocations (and you're right, I did not pay attention to the fact that I post increment cnt), but if the solution was that simple, why would the results vary that much across platforms and VM implementations? – flrnb Jul 24 '13 at 21:18
  • @flrnb That's because different platforms may affect the size of stack frame and different version of jre will affect the implementation of `System.out.print` or the method execution strategy. As described above, the VM implementation also affect where the stack frame will be actual stored. – Jay Jul 24 '13 at 21:45
0

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.

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

Now if you remove the try catch block as following

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

I used netbeans to see the output, as the cmd will not show all the output and exception thrown.

me_digvijay
  • 5,374
  • 9
  • 46
  • 83