Running on Java 24-ea+25-3155 (Preview)
Home of The JavaSpecialists' Newsletter

174Java Memory Puzzle Explained

Author: Dr. Heinz M. KabutzDate: 2009-06-26Java Version: 1.1 - 1.7Category: Performance
 

Abstract: In this newsletter, we reveal the answer to the puzzle from last month and explain the reasons why the first class sometimes fails and why the second always succeeds. Remember this for your next job interview ...

 

Welcome to the 174th issue of The Java(tm) Specialists' Newsletter. I've thoroughly enjoyed the last two weeks with my family in Crete, going to beaches, playing Age of Mythology with my two older kids and getting up early every morning to do exercise with my brother-in-law Costa, a professional gym instructor. As a result, I've done very little Java in the last two weeks, aside from answering the 400+ emails that I received as a result of last month's puzzle. Thank you all for participating in this puzzle - you can stop sending emails now ;-)

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Java Memory Puzzle Explained

The initial JavaMemoryPuzzle was based on a discussion that one of my readers, Dmitry Vyazelenko, had with some friends. They were arguing that you should always set local variables to null after you finish using them and were using code such as seen in our puzzle to prove their point. To understand why setting local variables to null is never necessary in the real world, we first need to understand why the first class fails.

Here is the JavaMemoryPuzzle code again:

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

  public void f() {
    {
      byte[] data = new byte[dataSize];
    }

    byte[] data2 = new byte[dataSize];
  }

  public static void main(String[] args) {
    JavaMemoryPuzzle jmp = new JavaMemoryPuzzle();
    jmp.f();
  }
}

The easiest way to understand why it fails is to look at the disassembled class file, which we can generate using the javap -c JavaMemoryPuzzle command. I will show just the relevant parts:

public void f();
  Code:
   0: aload_0
   1: getfield	#6; //Field dataSize:I
   4: newarray byte
   6: astore_1
   7: aload_0
   8: getfield	#6; //Field dataSize:I
  11: newarray byte
  13: astore_1
  14: return

Instruction 0 loads the pointer this onto the stack. Instruction 1 reads the value of dataSize and leaves that on the stack. Instruction 4 creates the new byte[] and leaves a pointer to it on the stack. Instruction 6 now writes the pointer to the byte[] into stack frame position 1. Instruction 7 and 8 again load the dataSize value onto the stack. Instruction 11 is now supposed to create a new byte[], but at this point, there is still a pointer to the old byte[] in the stack frame. The old byte[] can thus not be garbage collected, so the newarray byte instruction fails with an OutOfMemoryError.

The data variable is never used again after the object scope, but it is not necessarily cleared when we leave the block. Inside the byte code, there is no concept of code blocks as such. The generated code would be different if we did not have the code block, here is what would have been generated:

  public void f() {
    byte[] data = new byte[dataSize];
    byte[] data2 = new byte[dataSize];
  }

would result in byte code that does not reuse the stack frame 1, as we can see with instruction 13:

public void f();
  Code:
    0: aload_0
    1: getfield	#6; //Field dataSize:I
    4: newarray byte
    6: astore_1
    7: aload_0
    8: getfield	#6; //Field dataSize:I
   11: newarray byte
   13: astore_2
   14: return

The code block allows the compiler to generate code that reuses the stack frame for the next local variable and would be more akin to the following method f() :

  public void f() {
    byte[] data = new byte[dataSize];
    data = new byte[dataSize];
  }

When Does JavaMemoryPuzzle Not Fail?

Several newsletter fans sent me an email pointing out that certain versions of the JVM, such as BEA JRockit and IBM's JVM, do not fail with the first version.

Others pointed out that it also does not fail with the new G1 collector, which you can use with the following VM options: -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC

Futhermore, Christian Glencross pointed out that when you compile the JavaMemoryPuzzle class with Java 1.0 or 1.1, it optimizes away the body of method f(), thus becoming:

public void f();
  Code:
   0: return

The javac compiler in older versions of Java was a lot more aggressive in its optimizations. It saw that we never used the fields and simply removed them from the code.

Joakim Sandström discovered that when you start the class with -Xcomp, it also never fails. Since Java 2, the JIT compiler takes the place of the static compiler for optimizing our code.

Christian Glencross also sent me code demonstrating that if you call method f() with a small dataSize often enough, then the JIT compiler will kick in and optimize the code. The large array construction then passes also. In this example, we call the f() method 100.000 times with a small dataSize. The HotSpot compiler will thus in all likelihood pick up that this method is being called a lot and then optimize it for us. Since the optimizing occurs in a separate thread, we need to call the method often enough so that we can be sure that the code has been optimized by the time we call it with the large dataSize. If you still get the OutOfMemoryError, just increase the number. Incidentally, you can also call the method fewer times, for example 10.000 times, and then sleep for a second, giving the JIT compiler time to optimize the code.

Make sure that you use the server HotSpot compiler, since the client compiler does not seem to do this optimization:

public class JavaMemoryPuzzleWithHotspotWarmup {
  private int dataSize = 0;

  public void f() {
    {
      byte[] data = new byte[dataSize];
    }

    byte[] data2 = new byte[dataSize];
  }

  public static void main(String[] args) {
    JavaMemoryPuzzleWithHotspotWarmup jmp =
        new JavaMemoryPuzzleWithHotspotWarmup();
    jmp.dataSize = 10;
    for (int i = 0; i < 1000 * 1000; i++) {
      jmp.f();

    }
    jmp.dataSize = (int) (Runtime.getRuntime().maxMemory() * 0.6);

    jmp.f(); // probably no OutOfMemoryError
  }
}

As we can see, the HotSpot compiler does a great job of optimizing our code even if we do not explicitly set local variables to null after we finish using them. In 12 years of coding Java, I have not once needed to null out local variables. The JavaMemoryPuzzle class is an edge case, from which we must not draw the wrong conclusions.

Why Does the Polite Version Pass?

In our previous newsletter, we then showed a "polite" version of the Java Memory Puzzle, which passes on all JVMs that I know of:

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

  public void f() {
    {
      byte[] data = new byte[dataSize];
    }

    for(int i=0; i<10; i++) {
      System.out.println("Please be so kind and release memory");
    }
    byte[] data2 = new byte[dataSize];
  }

  public static void main(String[] args) {
    JavaMemoryPuzzlePolite jmp = new JavaMemoryPuzzlePolite();
    jmp.f();
    System.out.println("No OutOfMemoryError");
  }
}

The majority of responses were incorrect and suggested that the for() loop either gave the GC time to do its work during the System.out.println() or that there was some obscure synchronization / JVM / voodoo happening at that point.

Some of my readers realised that it had nothing to do with the System.out.println and that a simple int i = 0; would suffice. If you declare any local variable immediately after the code block, you break the strong reference to the byte[] held in the stack frame 1 before you invoke the new byte[] the second time. Again, looking at the generated byte code of the polite puzzle explains this nicely:

public void f();
  Code:
    0: aload_0
    1: getfield	#6; //Field dataSize:I
    4: newarray byte
    6: astore_1
    7: iconst_0
    8: istore_1
    9: iload_1
   10: bipush 	10
   12: if_icmpge 29
   15: getstatic #7; //Field System.out
   18: ldc    #8; //String Please be so kind and release memory
   20: invokevirtual #9; //Method PrintStream.println
   23: iinc   1, 1
   26: goto   9
   29: aload_0
   30: getfield	#6; //Field dataSize:I
   33: newarray byte
   35: astore_1
   36: return

After Instruction 8 has completed, we do not have a strong reference to the first byte[] left, so the GC can collect it at any time. The polite request "Please be so kind and release memory" was just to catch you out, and boy, was it successful :-)))

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '24 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...