1
头图
Like it first, then watch it, develop a good habit

Two days ago, a small partner suddenly asked me for help, saying that he was going to change a pit. Recently, he was learning multi-threading knowledge in the system, but he encountered a problem of refreshing his cognition...

Little buddy: In the concurrency chapter of Effective JAVA, there is a description about visibility. The following code will have an infinite loop. I can understand this. JMM memory model. JMM does not guarantee that the modification of stopRequested can be observed in time.
static boolean stopRequested = false;

public static void main(String[] args) throws InterruptedException {

    Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested) {
            i++;
        }
    }) ;
    backgroundThread.start();
    TimeUnit.MICROSECONDS.sleep(10);
    stopRequested = true ;
}
But the strange thing is that after I added a line to print, there would be no infinite loop! Could my line of println be better than volatile? These two are okay
static boolean stopRequested = false;

public static void main(String[] args) throws InterruptedException {

    Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested) {
            
            // 加上一行打印,循环就能退出了!
            System.out.println(i++);
        }
    }) ;
    backgroundThread.start();
    TimeUnit.MICROSECONDS.sleep(10);
    stopRequested = true ;
}

Me: The young man is quite familiar with the stereotyped essays. JMM opens his mouth and comes.

Me: This... is actually a good thing done by JIT, causing your loop to fail to exit. JMM is just a logical memory model, some internal mechanisms are related to JIT

For example, in your first example, if you use -Xint disable JIT, you can exit the infinite loop. If you don't believe me, try?

Little friend: Damn it, it's really ok, add -Xint loop and exit, it's amazing! What is JIT? Can it still have this effect?

JIT (Just-in-Time) optimization

As we all know, JAVA adds a layer of JVM in order to achieve cross-platform. JVMs of different platforms are responsible for interpreting and executing bytecode files. Although there is a layer of explanation that will affect efficiency, the advantage is that it is cross-platform and bytecode files are platform-independent.

image.png

After JAVA 1.2, (Just-in-Time Compilation, JIT for short) mechanism has been added, which can compile hot code with a high number of executions into machine code at runtime, so that there is no need for JVM to interpret it again. It can be executed directly to increase operating efficiency.


But when the JIT compiler compiles bytecode, it can not only simply translate the bytecode into machine code , it also does a lot of optimizations, such as loop unrolling, method inlining, etc...

The reason for this problem is that one of the optimization techniques of the JIT compiler- expression hoisting .

Expression hoisting

Let’s first look at an example. In this hoisting y is defined every time in the for loop, and then the result of x*y is stored in a result variable, and then this variable is used for various operations

public void hoisting(int x) {
    for (int i = 0; i < 1000; i = i + 1) {
        // 循环不变的计算 
        int y = 654;
        int result = x * y;
        
        // ...... 基于这个 result 变量的各种操作
    }
}

But in this example, the result of result is fixed and will not be updated in the loop. Therefore, the calculation of result can be extracted out of the loop, so there is no need to calculate each time. After JIT analysis, this code will be optimized to perform expression promotion operations:

public void hoisting(int x) {
    int y = 654;
    int result = x * y;
    
    for (int i = 0; i < 1000; i = i + 1) {    
        // ...... 基于这个 result 变量的各种操作
    }
}

In this way, the result does not need to be calculated every time, and the execution result is not affected at all, which greatly improves the execution efficiency.

Note that the compiler prefers local variables rather than static variables or member variables; because static variables are "escaped" and can be accessed by multiple threads, local variables are private to the thread and will not be accessed by other threads. modify.

When the compiler handles static variables/member variables, it will be conservative and will not easily optimize.

Like the example in your question, stopRequested is a static variable, and the compiler should not optimize it;

static boolean stopRequested = false;// 静态变量

public static void main(String[] args) throws InterruptedException {

    Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested) {
            // leaf method
            i++;
        }
    }) ;
    backgroundThread.start();
    TimeUnit.MICROSECONDS.sleep(10);
    stopRequested = true ;
}

But because your loop is a leaf method , that is, no method is called, so there will be no other threads in the loop that will observe the value of stopRequested Then aggressive compiler were expression lifting operation, the stopRequested raised to expression beyond, as a loop invariant (loop invariant) Processing:

int i = 0;

boolean hoistedStopRequested = stopRequested;// 将stopRequested 提升为局部变量
while (!hoistedStopRequested) {    
    i++;
}

In this way, the final stopRequested assigning 060bd84b2d3f0f to true hoistedStopRequested . Naturally, it will not affect the execution of the loop, which will eventually lead to failure to exit.

As for the problem that the loop can exit after println It is because your println code affects the optimization of the compiler. println method will eventually call FileOutputStream.writeBytes this native method, so it cannot be inline optimized (inling) . Without being restrained method call from the compiler's point of view is a "full memory kill", that is to say side effects unknown , have to do to read and write memory conservative management .

stopRequested in the next round of the cycle should occur sequentially after the println of the previous round. The "conservative treatment" here is: Even if I have read the stopRequested in the **unknown side effect**, I must read it again in the next visit.

So after you add prinltln, because the JIT needs to be conservatively processed and re-read, naturally it can't do the above expression promotion optimization.

Above for expression lifting interpretation summarized excerpt from large R & lt of know almost answered . R big, walking JVM Wiki!

Me: "Understand now, this is a good thing JIT does. If you disable JIT, there will be no problem."

Little friend: "Fuck 🐂🍺, a simple for loop is too much mechanism, I didn't expect JIT to be so smart, and I didn't expect R to be so big 🐂🍺"

Little partner: "Then JIT must have a lot of optimization mechanisms, besides this expression promotion, what else?"

Me: I'm not a compiler...Where I know so much, I know some commonly used ones. Let me tell you briefly.

Expression sinking

Similar to expression promotion, there is also an optimization for expression sinking, such as the following code:

public void sinking(int i) {
    int result = 543 * i;

    if (i % 2 == 0) {
        // 使用 result 值的一些逻辑代码
    } else {
        // 一些不使用 result 的值的逻辑代码
    }
}

Since the value of result is not used in the else branch, the result is calculated every time regardless of the branch, which is unnecessary. JIT will move the calculation expression of result to the if branch, thus avoiding every calculation of result. This operation is called expression sinking:

public void sinking(int i) {
    if (i % 2 == 0) {
        int result = 543 * i;
        // 使用 result 值的一些逻辑代码
    } else {
        // 一些不使用 result 的值的逻辑代码
    }
}

What other common optimizations are there in JIT?

In addition to the expression promotion/expression sinking described above, there are some common compiler optimization mechanisms.

Loop unwinding/loop unrolling

The following for loop needs to loop 10w times in total, and the conditions need to be checked each time.

for (int i = 0; i < 100000; i++) {
    delete(i);
}

After the compiler is optimized, a certain number of loops will be deleted, thereby reducing the overhead caused by index increment and condition checking operations:

for (int i = 0; i < 20000; i+=5) {
    delete(i);
    delete(i + 1);
    delete(i + 2);
    delete(i + 3);
    delete(i + 4);
}

In addition to loop unrolling, loops also have some optimization mechanisms, such as loop stripping, loop swapping, loop splitting, loop merging...

Inline optimization (Inling)

The JVM method call is a stack model. Each method call requires a push and pop operation. The compiler will also optimize the call model and inline some method calls.

Inlining is to extract the method body code to be called and execute it directly in the current method, so as to avoid the operation of pushing and popping the stack once and improve the execution efficiency. For example, the following method:

public  void inline(){
    int a = 5;
    int b = 10;
    int c = calculate(a, b);
    
    // 使用 c 处理……
}

public int calculate(int a, int b){
    return a + b;
}

After the compiler inline optimization, the method body of calculate inline method, and it will be executed directly without method call:

public  void inline(){
    int a = 5;
    int b = 10;
    int c = a + b;
    
    // 使用 c 处理……
}

However, this inline optimization has some limitations. such as native methods cannot inline optimization

Empty in advance

was finalized! look at an example first. In this example, 060bd84b2d4252 will be done. . This is also due to the optimization of JIT.

class A {
    // 对象被回收前,会触发 finalize
    @Override protected void finalize() {
        System.out.println(this + " was finalized!");
    }

    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        System.out.println("Created " + a);
        for (int i = 0; i < 1_000_000_000; i++) {
            if (i % 1_000_00 == 0)
                System.gc();
        }
        System.out.println("done.");
    }
}

//打印结果
Created A@1be6f5c3
A@1be6f5c3 was finalized!//finalize方法输出
done.

As can be seen from the example, if a is no longer used after the loop is completed, the finalize will be executed first; although from the object scope, the method is not executed and the stack frame is not popped, but Will be executed in advance.

This is because JIT believes that the a object will not be used in the loop or after the loop, so it is emptied in advance to help GC recycle; if JIT is disabled, this problem will not occur...

This early recycling mechanism is still a bit risky, and it will cause bugs in some scenarios, such as " A JDK thread pool BUG triggered GC mechanism thinking "

Various optimization items of HotSpot VM JIT

The above just introduces a few simple and commonly used compilation optimization mechanisms. For more optimization mechanisms of JVM JIT, please refer to the following figure. This is a pdf material provided in the OpenJDK document, which lists various optimization mechanisms of HotSpot JVM, quite a lot...

How to avoid problems caused by JIT?

Little friend: "JIT has so many optimization mechanisms, it is easy to go wrong, how do I usually avoid these when I write code"

Usually when coding, you don’t need to deliberately care about the optimization of JIT, such as the println problem above. JMM does not guarantee that the modification will be visible to other threads. If you lock or modify it with volatile according to the specification, there will be no such thing. Kind of problem.

As for the problem caused by blanking in advance, the probability of occurrence is also very low, as long as you write code in a standardized way, you will not encounter it.

Me: So, this is not JIT’s pot, it’s yours...

Little friend: "Understood, you are talking about my food, talking about the shit written by my code"

to sum up

In the daily coding process, there is no need to deliberately guess the optimization mechanism of the JIT, and the JVM will not fully tell you all the optimizations. And this kind of thing has different effects in different versions, even if you understand a mechanism, it may be completely different in the next version.

Therefore, if you are not engaged in compiler development, JIT-related compilation knowledge should be used as a knowledge reserve.

is no need to guess how the JIT will optimize your code, you (maybe) not sure...

reference

Originality is not easy, unauthorized reprinting is prohibited. If my article is helpful to you, please click

空无
3.3k 声望4.3k 粉丝

坚持原创,专注分享 JAVA、网络、IO、JVM、GC 等技术干货