Fundamentals 20 min read

When JIT Optimizes Away Safepoint Polls: Causes, Evidence, and Fixes

This article explains how JIT compilation can eliminate safepoint polls in hot loops, causing unexpected thread blocking, and demonstrates how to reproduce, verify, and resolve the issue using JITWatch and JVM options.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
When JIT Optimizes Away Safepoint Polls: Causes, Evidence, and Fixes

This article investigates why a loop that repeatedly calls AtomicInteger.getAndAdd can prevent the main thread from reaching a safepoint, leading to unexpected blocking.

The root cause is JIT optimization: the JIT compiler treats the loop body as hot code, removes the safepoint poll, and thus the thread cannot be stopped at a safepoint.

Because the loop body is identified as hot code, the JIT compilation removes the entry to the safepoint, so the thread cannot enter a safepoint inside the loop.

The original test case is:

public class MainTest {
    public static AtomicInteger num = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 1000000000; i++) {
                num.getAndAdd(1);
            }
            System.out.println(Thread.currentThread().getName() + " execution finished!");
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("num = " + num);
    }
}

When run with a JDK version higher than 10, the main thread waits for the child threads instead of printing after the sleep.

Changing the loop counter type from int to long or inserting Thread.sleep(0) restores the expected behavior because the loop is no longer treated as a counted hot loop.

Bold Hypothesis

The first guess is to blame the JIT compiler: the hot code num.getAndAdd(1) is optimized away, removing the safepoint.

Hot code in the JVM is defined as either frequently called methods or loops that execute many times.

When such code is compiled, the whole method is compiled, and the entry point (Byte Code Index) may differ, leading to On‑Stack Replacement (OSR) compilation.

Careful Verification

Disabling JIT with -Djava.compiler=NONE shows that the main thread proceeds after the sleep, confirming that the missing safepoint is caused by JIT optimization.

-Djava.compiler=NONE

Using the tool JITWatch you can compare the assembly generated at the C1 and C2 compilation stages. The presence of the {poll} instruction indicates a safepoint; it disappears after full C2 optimization.

Adding Thread.sleep(0) forces the compiler to keep the safepoint poll, preventing the aggressive optimization.

Key Article

The detailed explanation of safepoints can be found in nitsanw’s blog post “The Meaning, Side‑effects and Cost of Safepoints”.

http://psy-lob-saw.blogspot.com/2015/12/safepoints.html

Important sections include:

What is a Safepoint?

When is a thread at a safepoint?

Bringing a Java thread to a safepoint

For C1/C2 compiled code, safepoint polls are inserted at method entry/exit, on non‑counted loop back‑edges, and between bytecodes in interpreter mode.

When a method is inlined, the compiler removes these polls, which explains the observed behavior.

Examples from “牛哥”

Two representative cases are shown:

Example 0 – Long TTSP Hangs Application

public class WhenWillItExit {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            long l = 0;
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                for (int j = 0; j < Integer.MAX_VALUE; j++) {
                    if ((j & 1) == 1) l++;
                }
            }
            System.out.println("How Odd:" + l);
        });
        t.setDaemon(true);
        t.start();
        Thread.sleep(5000);
    }
}

The thread never stops after 5 seconds because the loop is optimized into a non‑counted hot loop.

Example 4 – Benchmark from Netty

A Netty issue discusses whether to use int or long in tight loops. The benchmark shows negligible difference, but the underlying JIT behavior still matters for safepoint insertion.

In summary, the missing safepoint poll is caused by JIT’s aggressive optimization of counted loops. Disabling JIT, using long counters, or inserting a harmless Thread.sleep(0) are practical ways to keep the safepoint and avoid the unexpected blocking.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaJVMJITSafepoint
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.