3

Could someone explain why this program throws a OutOfMemoryError when the for loop is commented out? If it is uncommented this runs fine.

The exception thrown is:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

public class JavaMemoryPuzzlePolite
{
    private final int dataSize = (int)(Runtime.getRuntime().maxMemory()* 0.6);

    public void f()
    {
        {
            System.out.println(dataSize);
            byte[] data = new byte[dataSize];
        }

        /*
        for(int i = 0; i < 10; i++) {
            System.out.println("Please be so kind and release memory");
        }
        */

        System.out.println(dataSize);
        byte[] data2 = new byte[dataSize];
    }

    public static void main(String []args)
    {
        JavaMemoryPuzzlePolite jmp = new JavaMemoryPuzzlePolite();
        jmp.f();
    }
}
Radiodef
  • 37,180
  • 14
  • 90
  • 125
indika
  • 821
  • 7
  • 13
  • 1
    Compiles fine on my machine, with or without the commented code – user000001 Jan 29 '14 at 17:05
  • 2
    Why do you have the extra bracket after f()? – mdewitt Jan 29 '14 at 17:05
  • 3
    @mdewitt It's just a block. – Sotirios Delimanolis Jan 29 '14 at 17:06
  • Exception in thread "main" java.lang.OutOfMemoryError: Java heap space – indika Jan 29 '14 at 17:06
  • 2
    It compiles correctly in both cases. It runs without an exception with the loop uncommented, but fails for me with OutOfMemoryError with the loop commented out. It would admittedly be easier to follow if you'd indented the code more clearly, but it's fine. If you're going to say there's an error, you should *always* state what the error is. – Jon Skeet Jan 29 '14 at 17:07
  • @JonSkeet I get an OutOfMemoryError. – Sotirios Delimanolis Jan 29 '14 at 17:09
  • The GC probably has time to run during the `System.out.println()` statements of the `for` loop. – GriffeyDog Jan 29 '14 at 17:09
  • I think compilation shouldn't be a problem. Do you mean "If it is uncommented this RUNs fine." instead of "compiles fine"? – NeplatnyUdaj Jan 29 '14 at 17:10
  • @SotiriosDelimanolis: Ah, doh - yes, so do I now. Had compiled but not *run* the first version. – Jon Skeet Jan 29 '14 at 17:10
  • The "If it is uncommented this compiles fine" in the question is misleading. After all, it *compiles* fine in both cases - it's only at *execution* time that it's a problem with the code commented out. It's really important to differentiate (both in your mind and in questions) between compile-time errors and execution-time errors. – Jon Skeet Jan 29 '14 at 17:12
  • Possibly Java isn't releasing the memory for the `byte[]` inside the standalone block before creating the next `byte[]`, and thus running out of memory because it would need 120% the available memory. – This company is turning evil. Jan 29 '14 at 18:49

3 Answers3

4

I've investigated this with many different types of code snippets that can be inserted where your comment is and the only type of code that will not cause an OutOfMemoryError is code that assigns some value to a local variable.

This is the explanation that makes the most sense to me:

When you have

byte[] data = new byte[dataSize];

The bytecode instructions are

    12: newarray       byte
    14: astore_1 

Where newarray creates a new array and astore_1 stores a reference to it in a local variable 1.

After this, the scope of that variable is lost, but the bytecode doesn't say anything about its value being cleared, so there's a reference to that object remaining in the stack frame. This specific garbage collector deems it reachable even if the code itself cannot reach it.

If instead you try to assign another local variable, like

byte i = 1;

then the corresponding byte code instructions are something like

    15: iconst_1      
    16: istore_1   

where iconst_1 stores the value 1 on the stack and istore_1 stores that value in the variable 1, which seems to be the same variable as before. If it is, then you are overwriting its value, the reference to the byte[] object is lost, and the object then "becomes" eligible for garbage collection.

The final proof

Compile this code with the -g option

public class Driver {
    private static final int dataSize = (int) (Runtime.getRuntime().maxMemory() * 0.6);

    public static void main(String[] args) throws InterruptedException {
        {
            System.out.println(dataSize);
            byte[] data = new byte[dataSize];

        }
        byte i = 1;
        System.out.println(dataSize);
        byte[] data2 = new byte[dataSize];
    }

}

and then run javap -c -l Driver. You will see a LocalVariableTable like so

LocalVariableTable:
  Start  Length  Slot  Name   Signature
   15       0     1    data   [B
    0      33     0    args   [Ljava/lang/String;
   17      16     1     i     B
   32       1     2    data2  [B

where slot is the index in astore_1 and istore_1. So you see, the reference to the byte[] is cleared when you assign a new value to the local variable. Even if the variables have different types/names, in bytecode, they are stored in the same place.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • +1: I'm not sure, but the call to System.out.println() uses the same local variable? – Martijn Courteaux Jan 29 '14 at 21:23
  • @MartijnCourteaux The `dataSize`? It's an instance variable. – Sotirios Delimanolis Jan 29 '14 at 21:24
  • No, I meant that somewhere in the execution of println the same slot is used. But now I see that `int i` int the loop is probably the variable that uses the same slot. – Martijn Courteaux Jan 29 '14 at 21:26
  • @MartijnCourteaux Yeah, didn't post the byte code for that one because there are a lot of instructions involved in a `for` loop, but the variable `i` would take up the same slot in the local variable table. – Sotirios Delimanolis Jan 29 '14 at 21:28
  • @MartijnCourteaux This whole exercise was an eye opener on how smartly (even in these situations) Java byte code was designed. – Sotirios Delimanolis Jan 29 '14 at 21:31
  • In which way is this "smart" ? This answer definitely requires more upvotes and you did very nice research. – Martijn Courteaux Jan 29 '14 at 21:41
  • @MartijnCourteaux I think it's smart how they are re-using the slot in the table. The table tells you for which lines a local variable is accessible. After that, if it's not accessible, there's no reason not to reuse the same space. – Sotirios Delimanolis Jan 29 '14 at 21:43
  • Yes, indeed. That is nice. But isn't this done with some basic graph theory and the graph coloring problem? The art is indeed to find a good solution to the coloring problem, but a heuristic greedy algorithm does pretty well. – Martijn Courteaux Jan 29 '14 at 21:46
  • 1
    http://en.wikipedia.org/wiki/Graph_coloring#Algorithms Here are some algorithms that can be used to assign variables to slots. The vertices of the graph are the variables, and the slots are represented by the colors. An edge between vertices implies that the two connected variables are alive at the same time. – Martijn Courteaux Jan 29 '14 at 21:49
  • 1
    Very nice to read this answer actually. I knew some of the rules of the behavior and abilities of the GC, but this simply explains the true reasons underneath for this question: http://stackoverflow.com/a/19482891/155137 – Martijn Courteaux Jan 29 '14 at 22:08
3

As the answer linked to by user Sotirios Delimanolis in the comments suggests, this has to do with the scope of the first data array. Simplifying a bit, the code

{
    byte[] data = new byte[bigNumber];
}
byte[] data = new byte[bigNumber];

compiles to (simplified and using English)

1: make an array of size bigNumber
2: store a reference to the created array into the 1st local variable
3: make an array of size bigNumber
4: store a reference to the created array into the 1st local variable

This runs out of memory since at step three 60% of the memory is already occupied and we are trying to create an array that takes up 60% more memory. The old byte array cannot be garbage collected because it is still referenced in the first local variable.

However the code

{
    byte[] data = new byte[bigNumber];
}
someOtherVariable = somethingSmall;
byte[] data = new byte[bigNumber];

compiles to

1: make an array of size bigNumber
2: store a reference to the created array into the 1st local variable
3: make somethingSmall
4: store a reference to somethingSmall into the 1st local variable
5: make an array of size bigNumber
6: store a reference to the created array into the 2nd local variable

Here at step four the initial large byte array is dereferenced. So when you try to make another large array at step five it succeeds because it can garbage collect the old array to make room.

SamYonnou
  • 2,068
  • 1
  • 19
  • 23
0

When you have the for loop uncommented, while executing the loop, JVM gets enough time to release the memory allocated for data since the scope of the variable has ended and the variable became unreachable.

When you comment out the for loop, JVM won't get much time to free the memory used by the varibale data

Nagendra Varma
  • 2,215
  • 3
  • 17
  • 26
  • Please note that in for loop , we are declaring a new variable `i`. When allocating the memory for `i`, GC is garbage collecting the memory allocated for the byte array `data`. If the variable is `i` defined before the defining the `data`, and uncomment the for loop, even then you will get `OutOfMemoryError` – Nagendra Varma Jan 30 '14 at 09:18
  • What does the variable have to do with GC? Your answer talks about time. In what sense? – Sotirios Delimanolis Jan 30 '14 at 13:02