Search code examples
javastack-overflowheap-memory

Heap Overflow And Stack Overflow examples


I need help in understanding heap and stack overflow, so it would be a lot helpful if I had few examples to understand the concept well


Solution

  • As the comments point out, there is a lot written about this on the internet already. Still, I am fan of doing hands-on research so here is some sample code (below) to get you started. I hope you find the following thought provoking... it is one thing to get an a simple answer, I think you'll find working through the data points so you understand what the answers mean more satisfying.

    The sample code (see below) is hardwired in main() to call either stress_heap() or stress_stack(). It pretty much dies so hard on my system that the catch blocks aren't helpful for reporting the results, so I added some intermediate print statements to show what is going on.

    Here are some charts (my first time posting images, so I hope it works). "cnt" represents the number of times stress_heap() or stress_stack() has been called. The memory values free & total are from the Java Runtime class. (total is how much the host system has granted the particular Java instance to work with, while free is how much of that total memory is avaialable for creating additional objects).

    plot of output when compiled for stress_heap() plot of output when compiled for stress_stack()

    The key thing for you to understand is available memory (heap) vs stack size. In Java all object instances live in heap. Object references (and primitives) are passed as params directly in the stack, which don't use the heap.

    Some questions to consider: when you can answer these you'll have a better understanding of heap vs. stack.

    • Why did the count for stress_heap() get so much larger than stress_stack() ?
    • Why does total memory increase for stress_heap() but not for stress_stack() ?
    • Why does free memory increase AND decrease for stress_heap() but not for stress_stack() ?

    Here is sample output that the above charts are based on (redundant lines omitted):

    output, compiled for stress_heap()

    $ java Stress
    stress_heap() cnt=100000 free=264613280 total=381157376
    stress_heap() cnt=200000 free=450105464 total=661127168
    stress_heap() cnt=300000 free=343009208 total=661127168
    stress_heap() cnt=400000 free=589030360 total=1009778688
    stress_heap() cnt=500000 free=485900632 total=1009778688
    ...45 lines deleted...
    stress_heap() cnt=5100000 free=327236008 total=5656018944
    stress_heap() cnt=5200000 free=214346344 total=5656018944
    stress_heap() cnt=5300000 free=115567888 total=5656018944
    stress_heap() cnt=5400000 free=16789432 total=5656018944
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at Stress.stress_heap(Stress.java:14)
        at Stress.main(Stress.java:45)
    $ 
    

    output, compiled for stress_stack()

    $ java Stress
    stress_stack_recurse() cnt=100 free=377151576 total=381157376
    stress_stack_recurse() cnt=200 free=377151576 total=381157376
    ...114 lines deleted...
    stress_stack_recurse() cnt=11700 free=377151576 total=381157376
    stress_stack_recurse() cnt=11800 free=377151576 total=381157376
    Exception in thread "main" java.lang.StackOverflowError
        at java.util.Arrays.copyOf(Arrays.java:2367)
        at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:130)
        at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:114)
        at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:415)
        at java.lang.StringBuilder.append(StringBuilder.java:132)
        at Stress.stress_stack_recurse(Stress.java:34)
        at Stress.stress_stack_recurse(Stress.java:36)
    ...1,015 lines deleted...
        at Stress.stress_stack_recurse(Stress.java:36)
        at Stress.stress_stack_recurse(Stress.java:36)
    $
    

    Stress.java - example program

    Depending on the system you run this on you may need to adjust the output intervals (the count thresholds) up or down.

    I encourage you to modify stress_stack_recurse() to add additional primitive arguments, something like:

    stress_stack_recurse1( int a ) { ... stress_stack_recurse(a+1); }
    stress_stack_recurse4( int a, int b, int c, int d )  { ... stress_stack_recurse4(a+1, b+1, c+1, d+1); }
    

    I think you'll want to modify arguments so compiler doesn't optimize them out, may need to add them to the conditional print to ensure they're used in the method body.

    The goal here is to see how high cnt gets before failing based on number of arguments.

    A similar research item would be to change the size value in stress_heap() and see how long that runs to 1024*16 (16kb) or 1024*1024 (1mb) size chunks.

    import java.util.*;
    public class Stress {
    
       static Runtime runtime = Runtime.getRuntime();
       static long cnt = 0;
    
       public static void stress_heap() {
          int size = 1024; // start with 1kb
          // research question: what happens to cnt as you modify size ?
          List<byte[]> a = new ArrayList<>(); // keep a list of byte arrays.
          System.out.println("stress_heap(): size="+size);
          try {
             while( true ) {
                byte[] b = new byte[size];
                a.add( b );
                ++cnt; // only increment if successful.
                if( 0 == (cnt % 100000) ) {
                   // Question: does this output display at the same tempo?
                   // Or does it get faster (or slower) as the program runs?
                   System.out.println("stress_heap() cnt="+cnt+" free="+runtime.freeMemory()+" total="+runtime.totalMemory() );
                }
             }
          } catch( Exception e ) { 
             // Sometimes fails so hard it doesn't reach this catch block.
             System.out.println("stress_heap(): problem, cnt="+cnt+", exception="+e);
          }
       }
    
       public static void stress_stack_recurse( ) { 
          ++cnt;
          if( 0 == (cnt % 100) ) {
             // Question: why does stress_stack_recurse() fail at such a low value count compared to stress_heap() ?
             // Question: does this output display at the same tempo?
             System.out.println("stress_stack_recurse() cnt="+cnt+" free="+runtime.freeMemory()+" total="+runtime.totalMemory() );
          }
          stress_stack_recurse( );
       }
    
       public static void stress_stack( ) { 
          // want the 
          try {
             stress_stack_recurse( );
          } catch( Exception e ) { 
             // Sometimes fails so hard it doesn't reach this catch block.
             System.out.println("stress_stack(): problem, cnt="+cnt+", exception="+e);
          }
       }
    
    
       public static void main( String[] args ) {
          //stress_heap();
          stress_stack();
       }
    }
    

    For bonus points, you could try passing in new byte arrays allocated in stress_stack() - any object really, but byte arrays seem easiest to reason about memory usage with.

    That could look something like this (this is untested code):

       public static void main( String[] args ) {
          //stress_heap();
          //stress_stack();
          byte[] b = new byte[1024];
          stress_both( b);
       }
    
       public static void stress_both( byte[] b ) {
          ++cnt;
          if( 0 == (cnt % 100) ) {
             System.out.println("stress_both() cnt="+cnt+" b.length="+b.length+" free="+runtime.freeMemory()+" total="+runtime.totalMemory() );
          }
          byte[] yetAnotherByteArray = new byte[1024];
          stress_stack_recurse( yetAnotherByteArray );
    
       }
    }