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.
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]; }
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.
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
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)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.