09 Oct 2012
6 mins readVolatile
Volatile Java keyword is already explained in many places around the web (like in What volatile Means in Java for example). So in this post I will not explain it once again, instead, I will focus on what it means in terms of JIT compiler native code production.
Safety tips: Do try this at home ! ;-)
So let’s take a classic example of boolean variable used to go out of a thread loop:
public class TestJIT
{
public static void main(String[] args) throws Exception
{
C c = new C();
c.start();
Thread.sleep(1*1000);
c.stopNow();
c.join();
}
private static class C extends Thread
{
private boolean running = true;
private int i;
@Override
public void run()
{
while (running)
{
i++;
}
if (System.currentTimeMillis() == 0)
{
System.out.println(i);
}
}
public void stopNow()
{
running = false;
}
}
}
Basically in this code, I start a thread, wait for 1 second and then notify the thread to stop by changing the running boolean to false.
If you compile and execute (in -server on windows x86 in my case) this code, you will observe that it never ends !
Easy, you think ! It’s because boolean is not volatile and the thread loop does not see the variable changing because it is not yet read from the main memory.
Ok makes sense, let’s see by ourself by disassembling the JIT code (I will explain how to do this in a next post):
[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
# {method} 'run' '()V' in 'TestJIT$C'
0x02608ec0: int3
0x02608ec1: xchg %ax,%ax
0x02608ec4: mov %eax,0xffffd000(%esp)
0x02608ecb: push %ebp
0x02608ecc: sub $0x18,%esp
0x02608ed2: mov (%ecx),%ebp
0x02608ed4: mov %ecx,(%esp)
0x02608ed7: call 0x6dbadeb0 ; {runtime_call}
0x02608edc: mov 0x4(%ebp),%ecx ; implicit exception: disp. to x02608f1d
0x02608edf: cmp $0x576e470,%ecx ; {oop('TestJIT$C')}
0x02608ee5: jne 0x02608f11 ;*aload_0
; - TestJIT$C::run@3 (line 24)
0x02608ee7: movzbl 0x70(%ebp),%ecx ;*getfield running
; - TestJIT$C::run@14 (line 22)
0x02608eeb: mov $0x1,%edi
0x02608ef0: add 0x6c(%ebp),%edi ;*iadd
; - TestJIT$C::run@9 (line 24)
0x02608ef3: mov %edi,0x6c(%ebp) ;*putfield i
; - TestJIT$C::run@10 (line 24)
0x02608ef6: test %ecx,%ecx
0x02608ef8: je 0x02608f06 ; OopMap{ebp=Oop off=58}
;*ifne
; - TestJIT$C::run@17 (line 22)
0x02608efa: test %edi,0x110000 ;*ifne
; - TestJIT$C::run@17 (line 22)
; {poll}
0x02608f00: inc %edi ;*iadd
; - TestJIT$C::run@9 (line 24)
0x02608f01: mov %edi,0x6c(%ebp) ;*putfield i
; - TestJIT$C::run@10 (line 24)
0x02608f04: jmp 0x02608efa
0x02608f06: mov $0xffffff86,%ecx
0x02608f0b: call 0x025ec700 ; OopMap{ebp=Oop off=80}
;*aload_0
; - TestJIT$C::run@13 (line 22)
; {runtime_call}
0x02608f10: int3
0x02608f11: mov $0xffffffad,%ecx
0x02608f16: nop
0x02608f17: call 0x025ec700 ; OopMap{ebp=Oop off=92}
;*aload_0
; - TestJIT$C::run@3 (line 24)
; {runtime_call}
0x02608f1c: int3 ;*getfield i
; - TestJIT$C::run@5 (line 24)
0x02608f1d: mov $0xfffffff6,%ecx
0x02608f22: nop
0x02608f23: call 0x025ec700 ; OopMap{off=104}
;*getfield i
; - TestJIT$C::run@5 (line 24)
; {runtime_call}
0x02608f28: int3 ;*getfield i
; - TestJIT$C::run@5 (line 24)
So here, you see the loop:
0x02608efa: test %edi,0x110000 ;*ifne
; - TestJIT$C::run@17 (line 22)
; {poll}
0x02608f00: inc %edi ;*iadd
; - TestJIT$C::run@9 (line 24)
0x02608f01: mov %edi,0x6c(%ebp) ;*putfield i
; - TestJIT$C::run@10 (line 24)
0x02608f04: jmp 0x02608efa
There is an unconditional jump (jmp instruction). It means that the JIT produced in fact an infinite loop as a while (true) i++;
There is also no emitted code for the rest of the run
method meaning that the test on System.currentTimeMillis
& and the System.out.println
lines were optimized away since they are now unreachable.
So let’s add the volatile keyword to our running boolean and see what happens. Firstly the code now works. Secondly, look at the generated code:
[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
# {method} 'run' '()V' in 'TestJIT$C'
0x026d8f00: int3
0x026d8f01: xchg %ax,%ax
0x026d8f04: mov %eax,0xffffd000(%esp)
0x026d8f0b: push %ebp
0x026d8f0c: sub $0x18,%esp
0x026d8f12: mov (%ecx),%ebp
0x026d8f14: mov %ecx,(%esp)
0x026d8f17: call 0x6dbadeb0 ; {runtime_call}
0x026d8f1c: mov 0x4(%ebp),%ecx ; implicit exception: disp. to 0x026d8f5d
0x026d8f1f: cmp $0x583e470,%ecx ; {oop('TestJIT$C')}
0x026d8f25: jne 0x026d8f51 ;*aload_0
; - TestJIT$C::run@3 (line 24)
0x026d8f27: incl 0x6c(%ebp) ;*putfield i
; - TestJIT$C::run@10 (line 24)
0x026d8f2a: jmp 0x026d8f37
0x026d8f2c: nopl 0x0(%eax)
0x026d8f30: mov 0x6c(%ebp),%ecx ;*getfield i
; - TestJIT$C::run@5 (line 24)
0x026d8f33: inc %ecx
0x026d8f34: mov %ecx,0x6c(%ebp) ;*aload_0
; - TestJIT$C::run@13 (line 22)
0x026d8f37: movzbl 0x70(%ebp),%ebx ; OopMap{ebp=Oop off=59}
;*ifne
; - TestJIT$C::run@17 (line 22)
0x026d8f3b: test %edi,0x180000 ; {poll}
0x026d8f41: test %ebx,%ebx
0x026d8f43: jne 0x026d8f30 ;*ifne
; - TestJIT$C::run@17 (line 22)
0x026d8f45: mov $0x18,%ecx
0x026d8f4a: nop
0x026d8f4b: call 0x026bc700 ; OopMap{ebp=Oop off=80}
;*invokestatic currentTimeMillis
; - TestJIT$C::run@20 (line 26)
; {runtime_call}
0x026d8f50: int3
0x026d8f51: mov $0xffffffad,%ecx
0x026d8f56: nop
0x026d8f57: call 0x026bc700 ; OopMap{ebp=Oop off=92}
;*aload_0
; - TestJIT$C::run@3 (line 24)
; {runtime_call}
0x026d8f5c: int3 ;*getfield i
; - TestJIT$C::run@5 (line 24)
0x026d8f5d: mov $0xfffffff6,%ecx
0x026d8f62: nop
0x026d8f63: call 0x026bc700 ; OopMap{off=104}
;*getfield i
; - TestJIT$C::run@5 (line 24)
; {runtime_call}
0x026d8f68: int3 ;*getfield i
; - TestJIT$C::run@5 (line 24)
Now we have something very different in the loop:
0x026d8f30: mov 0x6c(%ebp),%ecx ;*getfield i
; - TestJIT$C::run@5 (line 24)
0x026d8f33: inc %ecx
0x026d8f34: mov %ecx,0x6c(%ebp) ;*aload_0
; - TestJIT$C::run@13 (line 22)
0x026d8f37: movzbl 0x70(%ebp),%ebx ; OopMap{ebp=Oop off=59}
;*ifne
; - TestJIT$C::run@17 (line 22)
0x026d8f3b: test %edi,0x180000 ; {poll}
0x026d8f41: test %ebx,%ebx
0x026d8f43: jne 0x026d8f30 ;*ifne
; - TestJIT$C::run@17 (line 22)
We get the field i from memory into ecx register, we increment ecx, we move the result to the previous location, we load the running field from the memory into ebx register, and we test if it equals to 0, if not we jump to the first instruction of the loop.
In the comment you see mentioning a call to currentTimeMillis
, in fact, there is the same address for the 2 next calls. Since my loop was compiled with OSR optimization, (you can check with -XX:+PrintCompilation) this is a call to C2I adapter (Compiler to Interpreter) that is emitted to go back to interpreter mode for the rest of the method since the whole method is not yet considered as a hot spot.
Note: movzbl
is basically a move with zero padding.
So in conclusion, is there a bug into the server JIT (C2) ? I do no think so ! Without volatile keyword, C2 is free to optimize optimisticly (aggressively ?) the code since you do not put hard guarantee on your boolean as specified in the Memory Model of the JLS.