0

I got different execution result from the follow code if the Simple class field a was modified by final keyword.

If the a is a final field , this program will normally exit; If it's a plain field, this program will keep running all the time.

This situation only occurs in C2 compiler .

I thought this situation is related to visibility of the flag field in multi-threads environment.However, I try to observed the assembly code by hsdis ,and found the difference between with and without final keyword.

I found nothing difference.

Actually, I know the storing "final" field would not emit any assembly instructions on x86 platform. But why this situation came out? Are there some particular operations I don't know ?

Thanks for reading.

class MultiProcessorTask {

    private boolean flag= true;

    public void runMethod() {
        while (flag) {
            new Simple(1);
        }
    }

    public void stopMethod() {
        System.out.println("change 'flag' field ...");
        flag= false;
    }
}


class ThreadA extends Thread {

    private MultiProcessorTask task;

    ThreadA(MultiProcessorTask task) {this.task = task;}

    @Override
    public void run() {
        task.runMethod();
    }
}

class Simple {
    private int a;  // modify "a" as "final"

    Simple(int a) {this.a = a;}
}

public class TestRun {
    public static void main(String[] args) {
        MultiProcessorTask task = new MultiProcessorTask();
        ThreadA a = new ThreadA(task);
        a.start();
        task.stopMethod();
        System.out.println("it's over");
    }
}

The disassembly code output:

  • The runMethod in the final case:

the final case

  • The runMethod in the non-final case:

the non-final case

Leon Wong
  • 13
  • 3
  • 1
    I've just tried to run your code and in both cases the result is the same. – Amongalen Aug 09 '19 at 09:53
  • 1
    Maybe it got something to do with Java Memory Model. It might emit the memory barrier that lets both thread see same value of the `flag`. Thing is, right now task thread aren't guaranteed to observe any particular value of `flag`. You should mark the flag `volatile`. – M. Prokhorov Aug 09 '19 at 09:54
  • [This article](http://tutorials.jenkov.com/java-concurrency/java-memory-model.html) appears to be a competent enough description of the JMM. – M. Prokhorov Aug 09 '19 at 09:56
  • @Michael, JVM might consider changes to `a` as happens-before to changes to `flag` (though I'm not sure why what might be, maybe this is just a wrong simplified example, and in actual app it would). If that happens, making `a` final, and thus making its assignment and value predictable will indeed help. – M. Prokhorov Aug 09 '19 at 09:57
  • @Amongalen Thanks for reply ! Did you run this code by C2 complier ? With this VM Ops `-server -Xcomp -XX:-TieredCompilation ` . My Oracle JDK version is '1.8.0_221-b11' – Leon Wong Aug 09 '19 at 10:08
  • @M.Prokhorov Thanks for reply. I know volatile will work. I just want to figure out what storing a final field is really do? – Leon Wong Aug 09 '19 at 10:11
  • @Michael Actually , the final is related. But I do not know why. – Leon Wong Aug 09 '19 at 10:13
  • @M.Prokhorov I know ``final`` has an happens-before relationship , but how JVM ensure it? After storing a final field, the compiler will insert StoreStore/LoadStore memory barriers.While these two barriers are no-op on x86, which means the program will do nothing after storing an final field. However , it's wrong , in this case , it proves some operations is performed after storing an final field. But I don't know what it is. – Leon Wong Aug 09 '19 at 10:19
  • When I look at Java 8’s disassembly output, I *see* differences, but when using Java 9 or newer, the differences are gone and consequently, there is no behavioral difference anymore, whether the `final` modifier is present or not. – Holger Aug 09 '19 at 11:45
  • @Holger So could you please tell me what differences between them in your disassembly output ? I checked it out again by ``hsdis``, and didn't see any differences. – Leon Wong Aug 10 '19 at 03:08
  • @Holger And I see this [post](http://blog.manycupsofcoffee.com/2013/09/java-final-fields-on-x86-no-op.html) . It implies that the ``final`` may has no effects on machine code on x86 platform. – Leon Wong Aug 10 '19 at 03:13
  • 1
    Can you post the x86 disassembly you get with `-XX:+PrintOptoAssembly`? I don't have a debug version of the JVM – Margaret Bloom Aug 10 '19 at 08:04
  • @MargaretBloom I don’t have debug version either. I just installed ``hsdis-amd64.dylib`` into ``/lib/server/`` directory , and ran my code with VM ops ``-server -Xcomp -XX:-TieredCompilation -XX:+PrintAssembly``. – Leon Wong Aug 10 '19 at 08:15
  • 1
    @LeonWong Section 17.5 of the JLS explains what is happening: *An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.*. If you look a the disassembly for the `runMethod` you'll see that in the non final case the thread enters a loop where only polls are done (I assume you know how the JVM safepoints works) and cannot exit it. ... – Margaret Bloom Aug 10 '19 at 10:46
  • 1
    ... This is because, without the final field, there is nothing that force the update of the JVM state between each iteration of the loop.The final version instead, don't loop (though I'm not sure why the execution its an `hlt`). – Margaret Bloom Aug 10 '19 at 10:46
  • @MargaretBloom: There's an `int3` before `hlt`. I haven't looked at much JVM JITed code but maybe that traps back to the JVM? Or the `call` before that is supposed to be noreturn and it's just padding for alignment between functions. – Peter Cordes Aug 11 '19 at 00:19
  • @MargaretBloom Thanks for your great inspiration ! The ``safe point`` you mention before, which made me review the disassembly output of ``runMethod`` . As you said, I found the ``SAFEPOINT POLL`` is outside of the loop , while just only found this in `C2 Level 4/OSR ` case. So I tried to turn off ``OSR`` with this VM ops `-XX:-UseOnStackReplacement`, and I found that situation was gone even without `final` modifier. – Leon Wong Aug 11 '19 at 07:59

1 Answers1

3

You've disassembled the wrong compilation. I mean, there is a standalone compiled runMethod on both screenshots, however, it is never executed in reality. Instead, execution jumps from the interpreter to the OSR stub. You need to look for a compilation marked with % sign (which denotes on-stack replacement).

Compiled method (c2)     646  662 %           MultiProcessorTask::runMethod @ 0 (20 bytes)
                                  ^
                                 OSR

Here is difference in the compiled code between non-final and final cases. I left only the relevant part:

non-final

  0x000000000309ae31: test   %eax,-0x5aae37(%rip)   ; safepoint poll
  0x000000000309ae37: jmp    0x000000000309ae31     ; loop

final

  0x0000000002c3a3a0: test   %eax,-0x265a3a6(%rip)  ; safepoint poll
  0x0000000002c3a3a6: movzbl 0xc(%rbx),%r11d        ; load 'flag' field
  0x0000000002c3a3ab: test   %r11d,%r11d
  0x0000000002c3a3ae: jne    0x0000000002c3a3a0     ; loop if flag == true

Indeed, the first case is compiled to an inifite loop, while the second one retains the field check.

You see, in both cases there is no Simple instance allocation at all, and no field assignment either. So, it's not a matter of instructions used to compile the final field assignment, but rather a compiler level barrier which prevents from caching the flag field out of the loop.

But since the allocation is eliminated altogether, the barrier implied by final field assignment can go away, too. Here we see just a missed optimization opportunity. And in fact, this missed optimization was fixed in newer JVM versions. If you run the same example on JDK 11, there will be an infinite loop in both cases, regardless of the final modifier.

apangin
  • 92,924
  • 10
  • 193
  • 247
  • 1
    As said in [this comment](https://stackoverflow.com/questions/57427531/in-java-what-operations-are-involved-in-the-final-field-assignment-in-the-cons?noredirect=1#comment101336486_57427531), the fix is already in Java 9. As a side note, even without eliminating the object allocation, the optimization barrier is not necessary, at least not in that restrictive form. – Holger Aug 12 '19 at 07:19