This is the first day of my participation in Gwen Challenge
Make it a habit to like it first
A couple of days ago, a friend suddenly asked me for help, saying that I was going to change a pit. Recently, I was learning multi-threading knowledge in the system, but I encountered a cognitive refresh problem…
Buddy: The concurrency section in Effective JAVA has a description of visibility. The following code will have an infinite loop, I can understand this, JMM memory model, JMM does not guarantee that stopRequested changes can be observed in a timely manner.
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 ;
}
Copy the code
But the strange thing is that after I add a line of print, there is no dead loop! Would a line of println work better than volatile? These two are not related either
static boolean stopRequested = false;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while(! stopRequested) {// Add a line of print and the loop will exit!System.out.println(i++); }}); backgroundThread.start(); TimeUnit.MICROSECONDS.sleep(10);
stopRequested = true ;
}
Copy the code
I: young man eight-part essay back of quite familiar, JMM zhang kou come.
Me: Well… It’s the JIT that keeps you from exiting the loop. The JMM is just a logical memory model, with some internal mechanisms related to JIT
For example, in your first example, you can disable JIT with -xint to exit the loop.
Friend: Oh, yes! Add the -xint loop to exit. What is a JIT? Can it do that?
Just-in-time (JIT) optimization
As we all know, JAVA adds a layer of JVMS to make it cross-platform, with JVMS on different platforms responsible for interpreting and executing bytecode files. While having a layer of interpretation can affect efficiency, the benefit is that cross-platform, bytecode files are platform independent.After JAVA 1.2, it was addedJust-in-time Compilation (JIT)At run time, hot code that is executed more times can be compiled into machine code, so that the JVM does not need to explain again, can be directly executed, increasing the efficiency of the operation.
But the JIT compiler does more than simply translate bytecode directly into machine code when compiling bytecode. It also makes many optimizations during compilation, such as loop unwrapping, method inlining, and so on…
This problem occurs as a result of expression promotion, one of the JIT compiler’s optimization techniques.
In expression promotion
As an example, in this case, the for loop defines a variable y each time, and then uses that variable to perform various operations by storing the result of x*y in a result variable
public void hoisting(int x) {
for (int i = 0; i < 1000; i = i + 1) {
// loop invariant calculation
int y = 654;
int result = x * y;
/ /... Various operations based on the result variable}}Copy the code
But in this example, the result is fixed and does not update through the loop. So it’s perfectly possible to extract the result evaluation out of the loop so that it doesn’t have to be evaluated every time. After JIT analysis, this code will be optimized for expression promotion:
public void hoisting(int x) {
int y = 654;
int result = x * y;
for (int i = 0; i < 1000; i = i + 1) {
/ /... Various operations based on the result variable}}Copy the code
In this way, result does not need to be calculated every time, and the execution result is not affected at all, greatly improving the execution efficiency.
Note that the compiler prefers local variables to static or member variables; Because static variables are “runaway” and can be accessed by multiple threads, local variables are private to the thread and cannot be accessed or modified by other threads.
The compiler is conservative when dealing with static/member variables and is not easily optimized.
In the example in your question, stopRequested is a static variable that should not be optimized by the compiler;
static boolean stopRequested = false;// Static variables
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while(! stopRequested) {// leaf methodi++; }}); backgroundThread.start(); TimeUnit.MICROSECONDS.sleep(10);
stopRequested = true ;
}
Copy the code
However, since your loop is a Leaf method, i.e. no method is called, no other thread in the loop will notice the stopRequested change. The compiler then arbitrarily promotes the expression, lifting stopinvariant out of the expression as loop invariant:
int i = 0;
boolean hoistedStopRequested = stopRequested;// Promote stopRequested to a local variable
while(! hoistedStopRequested) { i++; }Copy the code
In this way, the operation that finally assigns stopRequested to true will not affect the value of the promoted hoistedStopRequested, so it will not affect the execution of the loop, and ultimately will not be able to exit.
As for the problem that once you add println, the loop can exit. Because your line of println code is affecting the compiler optimization. Because eventually call println method FileOutputStream. WriteBytes the native method, so I can’t be inline optimization (inling). Uncollared method calls, on the other hand, are a “full memory kill” from the compiler’s point of view, meaning that side effects are unknown and that reads and writes to memory must be treated conservatively.
In this case, the stopRequested reads for the next loop will occur sequentially after the println of the previous loop. The “conservative processing” here is: even if I have read the stopRequested value in the last round, I will have to read it again in the next visit due to an unknown side effect.
So after you add prinltln, the JIT will not be able to do the expression promotion optimization above because of the conservative processing and re-reading.
The above explanation of expression promotion is summarized from R big Zhihu’s answer. R large, walking JVM Wiki!
Me: “See, this is all JIT, if you disable THE JIT there will be no problem.”
Friend: “My god 🐂🍺, there are too many mechanisms for a simple for loop. I didn’t expect JIT to be so smart, and I didn’t expect R to be so 🐂🍺.”
Friend: “There must be many optimization mechanisms in JIT. What else is there besides this expression improvement?”
Me: I’m not a compiler… Which understand so much, know some commonly used, simple to say to you
Expression Sinking
Similar to expression promotion, there is an optimization for expression sinking, such as the following code:
public void sinking(int i) {
int result = 543 * i;
if (i % 2= =0) {
// Use some logical code for the result value
} else {
// Some logical code that does not use the value of result}}Copy the code
Since the value of result is not used in the else branch, it is not necessary to evaluate result every time no matter what branch it is. The JIT moves the result expression into the if branch to avoid sinking the result expression each time it is evaluated:
public void sinking(int i) {
if (i % 2= =0) {
int result = 543 * i;
// Use some logical code for the result value
} else {
// Some logical code that does not use the value of result}}Copy the code
What other common optimizations do JIT have?
In addition to the expression lifting/expression sinking described above, there are some common compiler optimization mechanisms.
Loop unwinding (Loop unrolling)
The next for loop will loop 10W times, checking the condition each time.
for (int i = 0; i < 100000; i++) {
delete(i);
}
Copy the code
After the compiler optimizes, a certain number of loops will be removed to reduce the overhead caused by index increments and condition checking:
for (int i = 0; i < 20000; i+=5) {
delete(i);
delete(i + 1);
delete(i + 2);
delete(i + 3);
delete(i + 4);
}
Copy the code
In addition to loop unwrapping, there are several optimization mechanisms for loops, such as loop stripping, loop swapping, loop splitting, loop merging…
Inline optimization (Inling)
The JVM’s method invocation is a stack model, and each method invocation requires a push and pop operation. The compiler also optimizes the invocation model to inline some method calls.
Inlining is to extract the code of the method body to be called and execute it directly in the current method. In this way, the operation of pushing off the stack at a time can be avoided and the execution efficiency can be improved. For example, this method:
public void inline(a){
int a = 5;
int b = 10;
int c = calculate(a, b);
// Use c to handle......
}
public int calculate(int a, int b){
return a + b;
}
Copy the code
After compiler inline optimization, extract the calculate method body into an inline method and execute it directly without making a method call:
public void inline(a){
int a = 5;
int b = 10;
int c = a + b;
// Use c to handle......
}
Copy the code
However, there are some limitations to this inline optimization. For example, native methods cannot be optimized inline
Empty in advance
In this example, it was King Louis xiv! Will be printed before done. This is also due to JIT optimization.
class A {
// Before objects are collected, Finalize will be triggered
@Override protected void finalize(a) {
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."); }}// Print the result
Created A@1be6f5c3
A@1be6f5c3 was finalized!// Finalize method output
done.
Copy the code
As can be seen from the example, if A is no longer used after the loop is completed, Finalize will be executed first. Although the method is not executed and the stack frame is not removed from the stack, it is still executed ahead of schedule from the object scope.
This is because the JIT thinks that the a object will not be used during or after the loop, so it is pre-empted to help the GC reclaim; If JIT is disabled, this problem will not occur…
This early collection mechanism is a bit risky and can cause bugs in certain scenarios, such as “A JDK thread pool BUG caused by GC mechanism considerations”.
HotSpot VM JIT optimizations
This is just a brief overview of some of the commonly used compiler optimization mechanisms, but the JVM JITTER can be found in the following figure for additional optimization mechanisms. This is a PDF material provided with the OpenJDK documentation that lists various optimizations for HotSpot JVM, quite a few…
How do I avoid JIT problems?
Friend: “There are so many JIT optimizations, it’s easy to go wrong. How can I avoid them when I write code?”
There is no need to worry about JIT optimizations when coding. As in the println issue above, the JMM does not guarantee that changes will be visible to other threads.
And that early empty caused by the problem, the probability of occurrence is very low, as long as you write the code is basically not encountered.
Me: So, it’s not JIT’s pot, it’s your…
Little friend: “Understand, you this is to say my dish, say my code to write shit……”
conclusion
You don’t have to guess what JIT optimizations are during routine coding, and the JVM doesn’t tell you all about optimizations. And this is something that works differently from version to version, and even if you figure out one mechanic, it can be completely different in the next version.
So, if you’re not doing compiler development, JIT compilation knowledge is a good source of knowledge.
Don’t try to guess how JIT will optimize your code, you (probably) won’t…
reference
- JSR-133 Java Memory Model and Thread Specification 1.0 Proposed Final Draft
- Oracle JVM Just-in-Time Compiler (JIT)
- JVM JIT-compiler overview – Vladimir Ivanov HotSpot JVM Compiler Oracle Corp.
- JVM JIT optimization techniques – part 2
- The Java platform – WikiBook
- R big Zhihu encyclopedia
Original is not easy, prohibit unauthorized reprint. Like/like/follow my post if it helps you ❤❤❤❤❤❤
Added a little
Some readers may think that sync is the cause of the problem, but here is an example of sync with some modifications, which still doesn’t get out of the loop…
public class HoistingTest {
static boolean stopRequested = false;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while(! stopRequested) {// Add a line of print and the loop will exit!
// System.out.println(i++);
newHoistingTest().test(); }}); backgroundThread.start(); TimeUnit.SECONDS.sleep(5);
stopRequested = true ;
}
Object lock = new Object();
private void test(a){
synchronized (lock){}
}
}
Copy the code
Add sync to the test method, and you can’t get out of the loop.
Object lock = new Object();
private synchronized void test(a){
synchronized (lock){}
}
Copy the code
But I just want to say that the crux of the problem is jit optimization caused the problem. The OPTIMIZATION mechanism of THE JIT is also part of the JMM. The JMM is just the specification, and the JIT is a mechanism in the VM implementation that will also follow the JMM specification.
The JMM doesn’t say that sync affects JIT or anything like that, but what if sync does… That’s not the point
The compiler is more sensitive to static variables. If you change the above lock object to static, you can exit the loop again…
What if instead of static, sync is unsafe.pagesize ()? The result is a loop that can still exit…
Therefore, the focus of this article is to describe the effects of JIT, rather than the various actions that can affect JIT. There are so many possibilities for jit to be affected, and different VMS and even different versions behave differently, we don’t need to know the mechanism, we can’t know it (after all, it’s not a compiler, it’s a compiler, it’s not necessarily HotSpot…).