有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java我可以通过字节码编译日志检测指令重新排序吗?

我有下一个代码示例:

class Shared {
    int x;
    int y;

    void increment() {
        x++;
        y++;
    }

    void check() {
        if (y > x) {
            System.out.println("Ooops! y > x");
        }
    }
}

看起来清楚吗?但当我尝试增加和签入两个线程时,主要问题发生在这里:

Shared shared = new Shared();

        Thread writer = new Thread(() -> {
            for (int i = 0; i < N; i++) {
                shared.increment();
            }
        });

        Thread reader = new Thread(() -> {
            for (int i = 0; i < N; i++) {
                shared.check();
            }
        });

writer.start();
reader.start();

您可能会注意到数据竞争(某些情况下指令会重新排序?)

1. x++;
2. y++;

现在,我知道了一些特殊的VM标志,它们可以帮助我打印JIT编译器日志(-XX:+PrintCompilation

...
    120  181       3       Shared::increment (21 bytes)
    120  182       3       Shared::check (20 bytes)
    120  183       4       Shared::increment (21 bytes)
    120  184       4       Shared::check (20 bytes)
    121  181       3       Shared::increment (21 bytes)   made not entrant
    121  182       3       Shared::check (20 bytes)   made not entrant
    121  185     n 0       java.lang.invoke.MethodHandle::linkToStatic(L)L (native)   (static)
    121  184       4       Shared::check (20 bytes)   made not entrant
    122  186       3       Shared::check (20 bytes)
    122  187     n 0       java.lang.Object::clone (native)   
    122  188       4       Shared::check (20 bytes)
    122  189 %     3       Main::lambda$main$0 @ 2 (19 bytes)
    122  190       3       Main::lambda$main$0 (19 bytes)
    123  186       3       Shared::check (20 bytes)   made not entrant
...

好的,现在我可以看到增量方法的编译是如何处理的:

    120  181       3       Shared::increment (21 bytes)
    120  183       4       Shared::increment (21 bytes)
    121  181       3       Shared::increment (21 bytes)   made not entrant

我是否理解正确的,这里的重新排序是由于tiered compilation?由于increment()-hot方法,JIT编译器会分析此信息并使用C2服务器编译器。而且,正如我所想,以这种方式重新排列一些指令,但在某些情况下会发生优化(made not entrant)。还是错了

此外,还有其他一些需要编译的日志:

    138  182       2       Shared::increment (21 bytes)
    138  184       4       Shared::increment (21 bytes)
    138  182       2       Shared::increment (21 bytes)   made not entrant

共 (1) 个答案

  1. # 1 楼答案

    这与分层编译无关。没有它,问题也会发生。让JVM只编译一个方法check,并查看它在C2编译代码中的外观:

    java -XX:-TieredCompilation \
         -XX:CompileCommand=compileonly,Shared::check \
         -XX:CompileCommand=print,Shared::check \
         Shared
    

    输出是

        0x00000000031a4160: mov     dword ptr [rsp+0ffffffffffffa000h],eax
        0x00000000031a4167: push    rbp
        0x00000000031a4168: sub     rsp,20h           ;*synchronization entry
                                                      ; - Shared::check@-1 (line 11)
    
    (1) 0x00000000031a416c: mov     r10d,dword ptr [rdx+0ch]
                                                      ;*getfield x
                                                      ; - Shared::check@5 (line 11)
    
    (2) 0x00000000031a4170: mov     r8d,dword ptr [rdx+10h]  ;*getfield y
                                                      ; - Shared::check@1 (line 11)
    
        0x00000000031a4174: cmp     r8d,r10d
        0x00000000031a4177: jnle    31a4185h          ;*if_icmple
                                                      ; - Shared::check@8 (line 11)
    
        0x00000000031a4179: add     rsp,20h
        0x00000000031a417d: pop     rbp
        0x00000000031a417e: test    dword ptr [1020000h],eax
                                                      ;   {poll_return}
        0x00000000031a4184: ret
    

    如您所见,x首先加载(第1行),然后加载y(第2行)。在这些行之间,另一个线程可能会增加y一个基数,从而使y看起来大于x

    在这种特殊情况下,您已经猜到了相对于原始程序顺序的加载的重新排序(在字节码中getfield ygetfield x之前)。然而,正如@Andreas所提到的,这并不是程序可能中断的唯一原因。即使JIT编译器在load(x)之前发出load(y),根据CPU体系结构的不同,第一个加载可能会得到一个较新的值,而第二个加载则会得到一个较旧的值,从JMM的角度来看,这是绝对正确的