Please enable Javascript to view the contents

Java Safepoint/Handshake 剖析求证 - JVM 必要之恶 - TL²;DR

 ·  ☕ 23 分钟

内容概述:Java Safepoint/Handshake 设计、实现原理剖析与求证 - 摘自我的开源书《面向技术宅的 JVM 内幕》Safepoint 一节。本文是摘录整合文章。建议用电脑或平板阅读本书原文,排版、图片、结构、整体阅读体验更友好。

Commonsense

Common Sense - Thomas Paine



大纲:

Necessary evil

在流行降本增笑的年代,什么东西都要说 light-weight 。应用程序框架要 light-weight,VM 要换成 light-weight 的 container,连女朋友也要找 light-weight 的。

那么,底层的编程语言 Runtime,更应该 light-weight 了。像 C++ 有自己的原则:

What is the zero-overhead principle? - isocpp.org

The zero-overhead principle is a guiding principle for the design of C++. It states that: What you don’t use, you don’t pay for (in time or space) and further: What you do use, you couldn’t hand code any better.

In other words, no feature should be added to C++ which would make any existing code (not using the new feature) larger or slower, nor should any feature be added for which the compiler would generate code that is not as good as a programmer would create without using the feature.

Zero-overhead principle - cppreference.com

The zero-overhead principle is a C++ design principle that states:

  1. You don’t pay for what you don’t use.
  2. What you do use is just as efficient as what you could reasonably write by hand.

The only two features in the language that do not follow the zero-overhead principle are runtime type identification and exceptions, and are why most compilers include a switch to turn them off.

还有一些就这个原则的讨论和实验:

Zero 固然好,但代价是 Reply Yourself 去做一些琐碎之事,如 memory 管理。于是很多语言出现了 GC。在 Rust 还未出来之前,大家都爱上有 GC 的语言,并认为 Stop The World ,或更小范围的 Stop some countries 是可以接受的 Necessary evil(必要之恶)。要知道就算当年如日中天的 Golang ,也要 GC 。更何况步入中年的 Java,以及在 G1 与 ZGC 之前 “臭名昭著” 的 Java GC/Stop The World 。于是,本文就要研究一下这个 Java Stop The World 的 Necessary evil

Necessary evil(必要之恶) 这个词,Wikipedia 上这样解释:

A necessary evil is an evil that someone believes must be done or accepted because it is necessary to achieve a better outcome—especially because possible alternative courses of action or inaction are expected to be worse. It is the “lesser evil” in the lesser of two evils principle, which maintains that given two bad choices, the one that is least bad is the better choice.

History

The Oxford Dictionary of Word Origins asserts that “[t]he idea of a necessary evil goes back to Greek”, describing the first necessary evil as marriage, and further stating that, “The first example in English, from 1547, refers to a woman”. Thomas Fuller, in his 1642 work, The Holy State and the Profane State, made another of the earliest recorded uses of the phrase when he described the court jester as something that “…some count a necessary evil in a Court”. In Common Sense, Thomas Paine described government as at best a “necessary evil”.

必要之恶 是指某人认为必须做或接受的恶,因为这是必要实现更好结果的必要条件 — 尤其是因为其他可能的行动或不作为方案预计会更糟。它是 两害相权取其轻原则 中的“两害相权取其轻”,该原则认为,在两个糟糕的选择中,最不糟糕的选择是更好的选择。

历史

牛津词源词典 断言“必要之恶的概念可以追溯到希腊语”,将第一个必要之恶描述为 婚姻,并进一步指出,“英语中的第一个例子来自 1547 年,指的是女性”。托马斯·富勒 在他 1642 年的作品 神圣国家与世俗国家 中,再次使用了该短语,当时他将 宫廷小丑 描述为“……有些人认为是宫廷中的必要之恶”。在《常识》一书中,托马斯·潘恩将 GOV 描述为“必要之恶”。

Safepoint 作为 Java 最让 end-user 讨厌,但又最让 JVM 实现者爱恨交织,重度依赖的机制。成为每个要研究 Java/JVM 的人都必须研究的机制。

image-20241007152649877

图: Safepoint 是 JVM 众多模块的依赖和协调机制 (来自: HotSpot JVM Deep Dive - Safepoint)

Safepoint 术语

先看看 Safepoint 相关知识的术语:

Safepoint / Safepointing / Stopping-the-world

来自: HotSpot JVM Deep Dive - Safepoint

  • Thread-local GC root := An oop , i.e. a pointer into the Java heap, local to a JavaThread. The denoted Java object is a root of a reachability tree.

  • Mutable thread state := A JavaThread state in which the thread can mutate the Java heap or its thread-local GC roots. Aka an unsafe state.

  • A Safepoint (noun) is a global JVM state

    • Intuition: At this point (state), the Java world is stopped. It is therefore safe, as in exclusive access, to inspect and process by the JVM.
    • Technical: No JavaThread is executing inside or can transition into a thread state classified as mutable
    • Technical: Thread-local GC roots for all JavaThreads are accessible (published) to the JVM
  • Safepointing (verb) or Stopping-the-world is a JVM process or mechanism to reach a Safepoint

    • Intuition, older notion: “The process of halting or stopping all executing Java threads”
    • Technical: The JVM cooperates with Java Threads using a technique called Cooperative Suspension
  • Cooperative Suspension is a poll-based technique

    • JavaThreads check or poll thread-local state at designated locations

    • On suspension, the JVM blocks JavaThreads from transitioning into thread states classified as mutable

    • On suspension, the JVM triggers JavaThreads to transition from a mutable into an immutable thread state. As a consequence, thread-local GC roots are published.

    • For example:

      • mov r10, qword ptr [r15+130h] // get thread-local poll page address
      • test dword ptr [r10], eax // try to read the poll page
  • Traditionally, bringing the system to a Safepoint has been a necessary evil for runtimes that provide some form of automatic memory management**

    • A pervasive JVM/Runtime mechanism. Consequently, a lot of machinery in the JVM.
    • But JVM developments, especially in the GC area, move ever closer to obviating the need for the global JVM safepoint state.
  • Thread-local GC root := JavaThread 本地的一个指向 heap 的 oop 。作为 GC 对象可达性分析树的树根

  • Mutable thread state := 指一类型的 JavaThread 的状态,在该状态下,线程可以改变 Java help 或其 Thread-local GC root。又称 unsafe state

  • Safepoint (名词) 是指一种 JVM 全局状态

    • 直觉上:此时(状态),Java 世界已停止。因此,JVM 检查和处理是安全的,就像独占访问一样。
    • 技术上:没有 JavaThread 在内部执行或可以转换为归类为 Mutable thread state 的线程
    • 技术上:所有 JavaThread 的Thread-local GC root都可以访问(发布)到 JVM
  • Safepointing(动词) 或 Stopping-the-world 是 JVM 达到安全点的过程或机制

    • 直觉上,较旧的概念:暂停或停止所有正在执行的 Java 线程的过程

    • 技术:JVM 使用一种称为Cooperative Suspension 协作挂起的技术与 Java 线程协作

  • 传统上,将系统置于Safepoint对于提供某种形式的自动内存管理的运行时来说是一种必要之恶

    • 但是 JVM 的发展,特别是在 GC 领域,已经越来越接近于消除对全局 JVM 安全点状态的需求。

Safepoint 流程概述

以上文字内容不太直观,来个图:

alt text

图: Stop The World 的步骤。Source: Async-profiler - manual by use cases

  1. Global safepoint request

    1.1 有一个线程向一个叫 VM Thread 提出了进入 safepoint 的请求,请求中带上 safepoint operation 参数,参数其实是 STOP THE WORLD(STW) 后要执行的 Callback 操作 。可能是触发 GC。也可能是其它原因。

    1.2 VM Thread 线程在收到 safepoint request 后,修改一个 JVM 全局的 safepoint flag 为 true(这个 flag 可以是操作系统的内存页权限标识) 。

    1.3 然后这个 VM Thread 就开始等待其它应用线程(App thread) 到达(进入) safepoint 。

    1.4 其它应用线程(App thread)其实会高频检查这个 safepoint flag ,当发现为 true 时,就到达(进入) safepoint 状态。

    源码 SafepointSynchronize::begin()

  2. Global safepoint

    VM Thread 发现所有 App thread 都到达 safepoint (真实的 STW 的开始) 。就开始执行 safepoint operationGC 操作safepoint operation 其中一种可能类型。

    源码 RuntimeService::record_safepoint_synchronized()

  3. End of safepoint operation

    safepoint operation 执行完毕, VM Thread 结束 STW 。

    源码 SafepointSynchronize::end()

JavaThread - State

详见本书的 JavaThread Polling 与 Reach Safepoint - JavaThread - State 一节。以下为摘录:

Safepoint 机制的实现依赖于 JavaThread

src/hotspot/share/runtime/javaThread.hpp

1
2
3
4
5
6
7
8
9
class JavaThread: public Thread {
...
  // Safepoint support
 public:                                                        // Expose _thread_state for SafeFetchInt()
  volatile JavaThreadState _thread_state;
 private:
  SafepointMechanism::ThreadData _poll_data;
  ThreadSafepointState*          _safepoint_state;              // Holds information about a thread during a safepoint
  address                        _saved_exception_pc;           // Saved pc of instruction where last implicit exception happened

src/hotspot/share/utilities/globalDefinitions.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// JavaThreadState keeps track of which part of the code a thread is executing in. This
// information is needed by the safepoint code.
//
// There are 4 essential states:
//
//  _thread_new         : Just started, but not executed init. code yet (most likely still in OS init code)
//  _thread_in_native   : In native code. This is a safepoint region, since all oops will be in jobject handles
//  _thread_in_vm       : Executing in the vm
//  _thread_in_Java     : Executing either interpreted or compiled Java code (or could be in a stub)
//
// Each state has an associated xxxx_trans state, which is an intermediate state used when a thread is in
// a transition from one state to another. These extra states makes it possible for the safepoint code to
// handle certain thread_states without having to suspend the thread - making the safepoint code faster.
//
// Given a state, the xxxx_trans state can always be found by adding 1.
//
enum JavaThreadState {
  _thread_uninitialized     =  0, // should never happen (missing initialization)
  _thread_new               =  2, // just starting up, i.e., in process of being initialized
  _thread_new_trans         =  3, // corresponding transition state (not used, included for completeness)
  _thread_in_native         =  4, // running in native code
  _thread_in_native_trans   =  5, // corresponding transition state
  _thread_in_vm             =  6, // running in VM
  _thread_in_vm_trans       =  7, // corresponding transition state
  _thread_in_Java           =  8, // running in Java or in stub code
  _thread_in_Java_trans     =  9, // corresponding transition state (not used, included for completeness)
  _thread_blocked           = 10, // blocked in vm
  _thread_blocked_trans     = 11, // corresponding transition state
  _thread_max_state         = 12  // maximum thread state+1 - used for statistics allocation
};

其中 class JavaThreadJavaThreadState _thread_state 字段记录了线程的状态。

HotSpot JVM Deep Dive - Safepoint 9-43 screenshot

图: JavaThread 状态机。Source: Java Thread state machine

来自: HotSpot JVM Deep Dive - Safepoint

This is the state machine for the java thread and we can further classify it into the following categories:

  • mutable thread state it’s a state in which the thread can mute it the java heap or its thread local gc routes
  • immutable thread states is a state where the threat can do none of these things
  • transition states which act like bridges between the mutable and the immutable states a transition state has a safe point check or a poll instruction together with appropriate fencing

这是 Java 线程的状态机,我们可以进一步将其分为以下类别:

  • mutable thread state 可变线程状态 线程可以修改 Java 堆或其线程本地 GC 数据
  • immutable thread states 不可变线程状态 不能修改 oop 的状态
  • transition states 过渡状态 充当mutable thread stateimmutable thread states 之间的桥梁,过渡状态具有 safe point check轮询指令 以及适当的隔离

来自: HotSpot JVM Deep Dive - Safepoint

Let’s for example take a look at this situation:
we have a new thread comes into being it starts running in the VM state.
Let’s say this thread now wants to execute some java code. In order to do that it will need to traverse a transition into the java state and as that the transition as we said contains a save point check. Some notable transitions here is that the java code(java state) can transition to VM state and to Native state without performing save point checks instead the save point check is performed when the thread returns to state java.

Another important takeaway here is that code executing in state native is considered safe this means that during a safe point java threads can actually continue running native code and this also means that counter to the intuitive notion that a safe point involves blocking or halting all java threads it only means that they do not executein a sense a sensitive mutable state

关于 transition states 的作用 ,让我们看一下这种情况:
我们有一个新的线程出现,一开始在 VM state 中运行。
假设这个线程现在要执行一些 Java 代码。为了做到这一点,它将需要间接跳转到 java state ,这个跳转包含 safepoint check。 值得注意的是,Java 代码(Java state) 可以直接跳转到 VM statenative state无需 执行 safepoint check,但在线程返回到 Java state 时执行,需要 safepoint check 。

另一个要注意的是,在native state下执行的代码被认为是安全的,这意味着在安全点期间,java 线程实际上可以继续运行 native code ,这也意味着,与安全点会阻塞或停止所有 java 线程的直观想法相反,安全点只意味着不执行敏感的 mutable state 操作。

GC oop trace

src/hotspot/share/runtime/javaThread.hpp

1
2
3
4
5
6
class JavaThread: public Thread {
...
  // Active_handles points to a block of handles
  JNIHandleBlock* _active_handles;
...
  JavaFrameAnchor _anchor;                       // Encapsulation of current java frame and it state

src/hotspot/share/runtime/javaFrameAnchor.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class JavaFrameAnchor {
...
 private:
  //
  // Whenever _last_Java_sp != nullptr other anchor fields MUST be valid!
  // The stack may not be walkable [check with walkable() ] but the values must be valid.
  // The profiler apparently depends on this.
  //
  intptr_t* volatile _last_Java_sp;

  // Whenever we call from Java to native we can not be assured that the return
  // address that composes the last_Java_frame will be in an accessible location
  // so calls from Java to native store that pc (or one good enough to locate
  // the oopmap) in the frame anchor. Since the frames that call from Java to
  // native are never deoptimized we never need to patch the pc and so this
  // is acceptable.
  volatile  address _last_Java_pc;

  // tells whether the last Java frame is set
  // It is important that when last_Java_sp != nullptr that the rest of the frame
  // anchor (including platform specific) all be valid.

  bool has_last_Java_frame() const                   { return _last_Java_sp != nullptr; }

来自: HotSpot JVM Deep Dive - Safepoint

Global jvm state the second clause was that thread local gc routes for all java threads are accessible or published to the jvm. All current garbage collectors are tracing collectors which means they follow or trace the reachability trees starting out from what is called a root set. That is a set of immediately available oops.

Proper subset of the route set is the set of routes that is local to and reachable from java threads.

Let’s take a look at what some of these thread local gc routes are.

oop Handles

  • Local jni handles

    A JavaThread has a field called JNIHandleBlock* _active_handles. A local jni handle provides indirect access to an oop for jni code running in state native . But allocating/deallocating and even dereferencing a jni handle involve first performing a vm state transition which will perform a safe point check. Local jni handles are auto managed so when the code returns from a jni method that is it transitions from state native doing a safe point check into state java the local jni handles allocated by that method are deallocated.

  • HandleArea (missed in OpenJDK21)

    The JavaThread also has a field called handle area and handle area and its companion the handle provides pretty much the same indirection functionality as a local jni handle but these are targeted for code running in the vm state. The important difference is that these handles are NOT auto managed but instead must be manually managed by the openjdk programmer. Handle marks are used to describe a handle scope. And the handle mark destructor will deallocate the allocated handles for that particular scope and the scopes can also be nested.

Last Java Frame

The thread also has an embedded struct called the JavaFrameAnchor _anchor field. It consists of three pointers:

  • _last_Java_sp for last java stack pointer

  • _last_Java_pc for last java program counter

  • _last_Java_fp(missed in OpenJDK21, because of virtual thread ?) for last java frame pointer. the last java frame is the entry point for external stack walking. It is set if a thread has at least one java activation record or frame on its stack and it’s currently not in state java. So the _last_Java_fp is set in state java before the thread transitions out. And conversely it is cleared upon thread reentry.

The anchor struct here requires only that the last java stack pointer is set as the other fields are either not relevant for that context or they can be derived by the stack walking code.

Java frames on the stack may contain ordinary narrow oops or derived oops. So if you compared to the handles we discussed previously these are naked oops that is they do not have a handling direction they are direct pointers.

  • An ordinary oop is a regular oop,
  • a narrow oop is a compressed version of an oop it’s a 32-bit size oop.
  • And the derived oop is a pointer into an object not pointing directly to its header.

So for example we can think of an a pointer that points out an element in an array and a derived oop is always associated with a base for a specific code position in java for a specific code position like a program counter which stack slots and registers contain oops relative to that pc is described by a piece of metadata generated by the compilers something called an oop map.

For a specific code position (pc), which stack slots and registers contain oops is described by a piece of metadata generated by the compilers, called an OopMap. To pinpoint an oop in a frame, the OopMap describes a location using a relative address, either from the frame stackpointer (sp) or as an index into a RegisterMap. Not all code positions have OopMaps; mainly call sites and safepoint poll page instructions. For stackwalks, the return address of each frame is associated with an OopMap.

JavaThread CPU Context

A thread executing Java code also has a CPU context. Per the calling convention and performance reasons, oops are ideally placed in registers. Hotspot widely employs something called Stubs or StubRoutines, which are special platform-specific assembly helper routines. An important feature of most Stubs is to save the CPU context when a thread leaves, or suspends its Java execution, and restoring it when the thread re-enters, resuming execution. A Register Map is used to resolve a location described by an OopMap to be in a register.

Safepoint 协作流程详述

Safepoint 协作流程可以划分为以下几步:

  1. 应用线程 Polling Safepoint
  2. 监听 Safepoint Request
  3. 接收 Safepoint Request
  4. Arm Safepoint - 标记所有线程
  5. 等待应用线程到达 Safepoint
  6. 应用线程陷入 Safepoint
  7. Global safepoint - The World Stopped
  8. Safepoint operation 结束
  9. Disarming Safepoint

应用线程 Polling Safepoint

详见本书的 JavaThread Polling 与 Reach Safepoint - Polling 一节。以下为摘录:

基础知识

JIT 生成代码的寄存器分类
固定寄存器
  • $r12 - 存放 Java Heap base
  • $r15 - 存放 thread local 的 JavaThread 指针
非固定(通用)寄存器在 Frame 间保存
  • $rbp - 由 callee-saved
  • 其它通用寄存器 - 由 caller-saved

Polling

Java 线程会高频检查 safepoint flag(safepoint check/polling) ,当发现为 true(arm) 时,就到达(进入) safepoint 状态。

JVM 初始化

JVM 在启动时,就已经初始化了两个 Memory Page ,用于 safepoint 。一个 bad_page 不可读,如在它上执行 test x86指令,线程会因收到信号而挂起并跳转到信号处理器代码 。一个 good_page 可读,可正常执行 test x86指令:

Stack:

libjvm.so!SafepointMechanism::default_initialize() (/jdk/src/hotspot/share/runtime/safepointMechanism.cpp:68)
libjvm.so!SafepointMechanism::pd_initialize() (/jdk/src/hotspot/share/runtime/safepointMechanism.hpp:56)
libjvm.so!SafepointMechanism::initialize() (/jdk/src/hotspot/share/runtime/safepointMechanism.cpp:171)
libjvm.so!Threads::create_vm(JavaVMInitArgs * args, bool * canTryAgain) (/jdk/src/hotspot/share/runtime/threads.cpp:492)
libjvm.so!JNI_CreateJavaVM_inner(JavaVM ** vm, void ** penv, void * args) (/jdk/src/hotspot/share/prims/jni.cpp:3577)
libjvm.so!JNI_CreateJavaVM(JavaVM ** vm, void ** penv, void * args) (/jdk/src/hotspot/share/prims/jni.cpp:3668)
libjli.so!InitializeJVM(JavaVM ** pvm, JNIEnv ** penv, InvocationFunctions * ifn) (/jdk/src/java.base/share/native/libjli/java.c:1506)
libjli.so!JavaMain(void * _args) (/jdk/src/java.base/share/native/libjli/java.c:415)
libjli.so!ThreadJavaMain(void * args) (/jdk/src/java.base/unix/native/libjli/java_md.c:650)
libc.so.6!start_thread(void * arg) (pthread_create.c:442)
libc.so.6!clone3() (clone3.S:81)

src/hotspot/share/runtime/safepointMechanism.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
uintptr_t SafepointMechanism::_poll_word_armed_value;
uintptr_t SafepointMechanism::_poll_page_armed_value;

//   const static intptr_t _poll_bit = 1;

void SafepointMechanism::default_initialize() {
  // Poll bit values
  _poll_word_armed_value    = poll_bit();
  _poll_word_disarmed_value = ~_poll_word_armed_value;

...
    // Polling page
    const size_t page_size = os::vm_page_size();
    const size_t allocation_size = 2 * page_size;
    char* polling_page = os::reserve_memory(allocation_size);
    os::commit_memory_or_exit(polling_page, allocation_size, false, "Unable to commit Safepoint polling page");
    MemTracker::record_virtual_memory_type((address)polling_page, mtSafepoint);

    char* bad_page  = polling_page;
    char* good_page = polling_page + page_size;

    os::protect_memory(bad_page,  page_size, os::MEM_PROT_NONE);
    os::protect_memory(good_page, page_size, os::MEM_PROT_READ);

    log_info(os)("SafePoint Polling address, bad (protected) page:" INTPTR_FORMAT ", good (unprotected) page:" INTPTR_FORMAT, p2i(bad_page), p2i(good_page));

    // Poll address values
    _poll_page_armed_value    = reinterpret_cast<uintptr_t>(bad_page); // <<<<<<<
    _poll_page_disarmed_value = reinterpret_cast<uintptr_t>(good_page); // <<<<<<<
    _polling_page = (address)bad_page; // <<<<<<<
}

(do_polling)=

真正 Polling

先看看相关的数据结构:

src/hotspot/share/runtime/javaThread.hpp

1
2
3
4
5
6
class JavaThread: public Thread {
    ...
 private:
  SafepointMechanism::ThreadData _poll_data;
  ThreadSafepointState*          _safepoint_state;              // Holds information about a thread during a safepoint
  address                        _saved_exception_pc;           // Saved pc of instruction where last implicit exception happened

src/hotspot/share/runtime/safepointMechanism.hpp

1
2
3
4
5
6
7
8
9
class SafepointMechanism {
...
  static address _polling_page;
...    
  struct ThreadData {
    volatile uintptr_t _polling_word;
    volatile uintptr_t _polling_page;
    ...
  };

从上面代码,可以猜到 SafepointMechanism._polling_page 是个 Global var。对应着 Global Safepoint。 而 JavaThread._poll_data._polling_page 是 Thread Local 的,对应着 Thread-Local Handshakes 。

自从 OpenJDK10 的 JEP 312: Thread-Local Handshakes - 2017年 后,就有了非 JVM Global 的 Safepoint - Thread Safepoint 。而 JVM Global 的 Safepoint 好像也修改为基于 Thread-Local Handshakes 去实现,即对每一条 JavaThread 执行 Thread-Local Handshakes

在 OpenJDK10 时,可以通过 -XX:-ThreadLocalHandshakes 去禁用 ThreadLocalHandshakes ,但以下几个过程后就不可以禁用了:

  • Deprecated in JDK13
  • Obsoleted in JDK14
  • Expired in JDK15

原因当然是 OpenJDK 已经强依赖于这个特性了: Obsolete ThreadLocalHandshakes - bugs.openjdk.org .

JIT 编译后的 Polling

可以用下图说明 polling_page 的切换:

safepoint-switch-poll-page.png

图: polling_page 的切换. Source: The Inner Workings of Safepoints 2023 - mostlynerdless.de

image-20241015150032675

图: JavaThread 与 R15 寄存器. Source: Robbin Ehn: Handshaking HotSpot - Youtube Java Channel - 2020

上图意为,读取本线程对应的 JavaThread._poll_data(SafepointMechanism::ThreadData).polling_page 指向的地址。其中 R15 寄存器一般会指向本线程对应的 JavaThread。

1
2
3
4
5
// Generated poll in JIT 
// poll-offset: JavaThread._poll_data(SafepointMechanism::ThreadData).polling_page 在 JavaThread 的 offset
// thread_reg: 一般指 R15 寄存器,用于保存本线程对应的 JavaThread
mov poll-offset + thread_reg, reg 
test rax, reg

Source: Robbin Ehn: Handshaking HotSpot - Youtube Java Channel - 2020

上面显示需要两条机器指令,才能完成 polling。如果你看过 OpenJDK11 之前的资料,之前应该就一条机器指令就够了:

test   DWORD PTR [rip+0xa2b0966],eax  

主要原因是 OpenJDK11 默认启用 JEP 312: Thread-Local Handshakes 的设计,要求每条 Thread 有自己的 polling_page 指针,所以需要多一条机器命令来多一层寻址。

####### JIT Polling 实验

下面,用实验观察的方法 fact check 一下。直接采用本书的 Stack Memory Anatomy - 堆栈内存剖析 - Java Options 一节中的示例环境、程序、输出。尝试在 java ... -XX:+PrintAssembly ... -XX:LogFile=./round3/mylogfile.log 输出的 JIT 汇编 mylogfile.log 文件中,找出 Polling 指令。

  1. 启动 GDB Debugger,见 Stack Memory Anatomy - 堆栈内存剖析 - 启动 debugger 一节
  2. Inspect JavaThread object layout。详见 GDB JVM FAQ - Inspect Object Layout 一节
(gdb) ptype /xo 'Thread'
/* offset      |    size */  type = class Thread : public ThreadShadow {
                             private:
                               static class Thread *_thr_current;
/* 0x0020      |  0x0008 */    uint64_t _nmethod_disarmed_guard_value;
...                             public:
/* 0x048c      |  0x0004 */    volatile enum JavaThreadState _thread_state;
                             private:
/* 0x0490      |  0x0010 */    struct SafepointMechanism::ThreadData {
/* 0x0490      |  0x0008 */        volatile uintptr_t _polling_word;
/* 0x0498      |  0x0008 */        volatile uintptr_t _polling_page;

                                   /* total size (bytes):   16 */
                               } _poll_data;
/* 0x04a0      |  0x0008 */    class ThreadSafepointState *_safepoint_state;
/* 0x04a8      |  0x0008 */    address _saved_exception_pc;
  1. 找到 Poll 指令

可见,_polling_page 的 offset 为 0x0498,即 0x498 。于是,在 mylogfile.log 中找 0x498 。发现几百个,抽其中一个:

[Entry Point]
  # {method} {0x00007ffbf4249ab8} &apos;enqueue&apos; &apos;(Ljava/util/concurrent/locks/AbstractQueuedSynchronizer$ConditionNode;)V&apos; in &apos;java/util/concurrent/locks/AbstractQueuedSynchronizer&apos;
  # this:     rsi:rsi   = &apos;java/util/concurrent/locks/AbstractQueuedSynchronizer&apos;
  # parm0:    rdx:rdx   = &apos;java/util/concurrent/locks/AbstractQueuedSynchronizer$ConditionNode&apos;
  #           [sp+0x40]  (sp of caller)
...  
  0x00007fffed738747:   call   0x00007fffed1170a0           ; ImmutableOopMap {[0]=Oop [16]=Derived_oop_[0] [8]=Oop [24]=Oop }
                                                            ;*invokevirtual setPrevRelaxed {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - java.util.concurrent.locks.AbstractQueuedSynchronizer::enqueue@31 (line 614)
                                                            ;   {optimized virtual_call}
  0x00007fffed73874c:   nop    DWORD PTR [rax+rax*1+0x23c]  ;   {other}
...
  0x00007fffed738786:   mov    BYTE PTR [r9+r10*1],0x0      ;*ifeq {reexecute=0 rethrow=0 return_oop=0}
                                                            ; - java.util.concurrent.locks.AbstractQueuedSynchronizer::enqueue@40 (line 615)
 ;; B8: #	out( B3 B9 ) &lt;- in( B7 B6 )  Freq: 8.17349
  0x00007fffed73878b:   mov    r10,QWORD PTR [r15+0x498]    ; ImmutableOopMap {rdx=Oop [0]=Oop r8=Derived_oop_[0] [24]=Oop }
                                                            ;*ifeq {reexecute=1 rethrow=0 return_oop=0}
                                                            ; - (reexecute) java.util.concurrent.locks.AbstractQueuedSynchronizer::enqueue@40 (line 615)
  0x00007fffed738792:   test   DWORD PTR [r10],eax          ;   {poll}
  0x00007fffed738795:   test   r11d,r11d
  0x00007fffed738798:   je     0x00007fffed738722

其中

0x00007fffed73878b:   mov    r10,QWORD PTR [r15+0x498]
0x00007fffed738792:   test   DWORD PTR [r10],eax

即为 Safepoint polling。 上文已经介绍过, r15 寄存器保存 Thread Local 的 JavaThread 对象指针。还可以看到一些 OopMap 的身影。

Java 源码:

src/java.base/share/classes/java/util/concurrent/locks/AbstractQueuedSynchronizer.java

606:     final void enqueue(ConditionNode node) {
607:         if (node != null) {
608:             boolean unpark = false;
609:             for (Node t;;) {
610:                 if ((t = tail) == null && (t = tryInitializeHead()) == null) {
611:                     unpark = true;             
612:                     break;
613:                 }
614:                 node.setPrevRelaxed(t);        // <<<< Safe point polled after call
615:                 if (casTail(t, node)) { 
616:                     t.next = node;
617:                     if (t.status < 0)         
618:                         unpark = true;
619:                     break;
620:                 }
621:             }
622:             if (unpark)
623:                 LockSupport.unpark(node.waiter);
624:         }
625:     }
  1. 进一步探索

这时,再看看 JavaThread 的属性。要知道 JavaThread 的属性,首先要知道 JavaThread 的地址。这时用 jhsdb:

hsdb> threads
513811 main
State: BLOCKED
Stack in use by Java: 0x00007ffff5285740 .. 0x00007ffff5285830
Base of Stack: 0x00007ffff5287000
Last_Java_SP: 0x00007ffff5285740
Last_Java_FP: null
Last_Java_PC: 0x00007fffed72c9af
Thread id: 513811

hsdb> threadcontext 513811
Thread "main" id=513811 Address=0x00007ffff002b3c0

可见,main JavaThread 的地址为 Address=0x00007ffff002b3c0 。hsdb inspect 一下:

hsdb> inspect 0x00007ffff002b3c0
Type is JavaThread (size of 1904)
oop ThreadShadow::_pending_exception: null
char* ThreadShadow::_exception_file: char @ null
int ThreadShadow::_exception_line: 0
ThreadLocalAllocBuffer Thread::_tlab: ThreadLocalAllocBuffer @ 0x00007ffff002b580
jlong Thread::_allocated_bytes: 0
ResourceArea* Thread::_resource_area: ResourceArea @ 0x00007ffff0018ba0
LockStack JavaThread::_lock_stack: LockStack @ 0x00007ffff002bae8
OopHandle JavaThread::_threadObj: OopHandle @ 0x00007ffff002b770
OopHandle JavaThread::_vthread: OopHandle @ 0x00007ffff002b778
OopHandle JavaThread::_jvmti_vthread: OopHandle @ 0x00007ffff002b780
OopHandle JavaThread::_scopedValueCache: OopHandle @ 0x00007ffff002b788
JavaFrameAnchor JavaThread::_anchor: JavaFrameAnchor @ 0x00007ffff002b798
oop JavaThread::_vm_result: null
Metadata* JavaThread::_vm_result_2: Metadata @ null
ObjectMonitor* JavaThread::_current_pending_monitor: ObjectMonitor @ null
bool JavaThread::_current_pending_monitor_is_from_java: 1
ObjectMonitor* JavaThread::_current_waiting_monitor: ObjectMonitor @ null
uint32_t JavaThread::_suspend_flags: 0
oop JavaThread::_exception_oop: null
address JavaThread::_exception_pc: address @ 0x00007ffff002b918
int JavaThread::_is_method_handle_return: 0
address JavaThread::_saved_exception_pc: address @ 0x00007ffff002b868
JavaThreadState JavaThread::_thread_state: 10
OSThread* JavaThread::_osthread: OSThread @ 0x00007ffff002d9a0
address JavaThread::_stack_base: address @ 0x00007ffff002b718
size_t JavaThread::_stack_size: 1048576
vframeArray* JavaThread::_vframe_array_head: vframeArray @ null
vframeArray* JavaThread::_vframe_array_last: vframeArray @ 0x00007ffff031da90
JNIHandleBlock* JavaThread::_active_handles: JNIHandleBlock @ 0x00007ffff01686f0
JavaThread::TerminatedTypes JavaThread::_terminated: 57002

还是 gdb 的信息会比 hsdb 多:

$1 = (class JavaThread *) 0x7ffff002b3c0
(gdb) p *((JavaThread*)0x00007ffff002b3c0)
$2 = {<Thread> = {<ThreadShadow> = {<CHeapObj<(MEMFLAGS)2>> = {<No data fields>}, _vptr.ThreadShadow = 0x7ffff7b4c4c8 <vtable for JavaThread+16>, _pending_exception = 0x0, _exception_file = 0x0, _exception_line = 0}, _nmethod_disarmed_guard_value = 1, ...

(gdb) p /x ((JavaThread*)0x00007ffff002b3c0)->_poll_data._polling_page
$4 = 0x7ffff7fa1000

然后,在 这前 pmap 的输出文件 pmap.txt 中找到:

         Address Perm   Offset Device    Inode     Size     Rss     Pss Referenced Anonymous LazyFree ShmemPmdMapped FilePmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked THPeligible Mapping

    7ffff7fa1000 ---p 00000000  00:00        0        4       0       0          0         0        0              0             0              0               0    0       0      0           0 
    7ffff7fa2000 r--p 00000000  00:00        0        4       0       0          0         0        0              0             0              0               0    0       0      0           0 

可见,在 core dump 时,thread local 的 _polling_page 指向了 bad page(没有 r Perm) 。即是 arming 状态。

再好奇一下 SafepointMechanism 的 static page 指针。

p/x SafepointMechanism::_poll_page_armed_value
$5 = 0x7ffff7fa1000 (Perm:---p)
p/x SafepointMechanism::_poll_page_disarmed_value
$6 = 0x7ffff7fa2000 (Perm:r--p)
p/x SafepointMechanism::_polling_page
$7 = 0x7ffff7fa1000

####### 更多 JIT Polling 实现方式

以上是 JIT Polling 方式的一个重要实现方式,JIT 对于不同类型的场景,可能会使用不同的方式。如: //TBD .

Non-JIT Polling

//TBD

监听 Safepoint Request

见本书的 VM Operations 一节。以下为摘录:

VMThread 线程作为协调者(coordinator) ,循环监听 safepoint request 队列中的 VM_Operation 请求,并执行队列中的操作。

src/hotspot/share/runtime/vmOperation.hpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// VM_Operation 的类型
// Note: When new VM_XXX comes up, add 'XXX' to the template table.
#define VM_OPS_DO(template)                       \
  template(Halt)                                  \
  template(SafepointALot)                         \
  template(Cleanup)                               \
  template(ThreadDump)                            \
  template(PrintThreads)                          \
  template(FindDeadlocks)                         \
  template(ClearICs)                              \
  template(ForceSafepoint)                        \
  template(DeoptimizeFrame)                       \
  template(DeoptimizeAll)                         \
  template(ZombieAll)                             \
  template(Verify)                                \
  template(HeapDumper)                            \
  template(CollectForMetadataAllocation)          \
  template(CollectForCodeCacheAllocation)         \
  template(GC_HeapInspection)                     \
  template(GenCollectFull)                        \
  template(GenCollectForAllocation)               \
  template(ParallelGCFailedAllocation)            \
  template(ParallelGCSystemGC)                    \
  template(G1CollectForAllocation)                \
  template(G1CollectFull)                         \
  template(G1PauseRemark)                         \
  template(G1PauseCleanup)                        \
  template(G1TryInitiateConcMark)                 \
  template(ZMarkEndOld)                           \
  template(ZMarkEndYoung)                         \
  template(ZMarkFlushOperation)                   \
  template(ZMarkStartYoung)                       \
  template(ZMarkStartYoungAndOld)                 \
  template(ZRelocateStartOld)                     \
  template(ZRelocateStartYoung)                   \
  template(ZRendezvousGCThreads)                  \
  template(ZVerifyOld)                            \
  template(XMarkStart)                            \
  template(XMarkEnd)                              \
  template(XRelocateStart)                        \
  template(XVerify)                               \
  template(HandshakeAllThreads)                   \
  template(PopulateDumpSharedSpace)               \
  template(JNIFunctionTableCopier)                \
  template(RedefineClasses)                       \
  template(GetObjectMonitorUsage)                 \
  template(GetAllStackTraces)                     \
  template(GetThreadListStackTraces)              \
  template(VirtualThreadGetStackTrace)            \
  template(VirtualThreadGetFrameCount)            \
  template(ChangeBreakpoints)                     \
  template(GetOrSetLocal)                         \
  template(VirtualThreadGetOrSetLocal)            \
  template(VirtualThreadGetCurrentLocation)       \
  template(ChangeSingleStep)                      \
  template(SetNotifyJvmtiEventsMode)              \
  template(HeapWalkOperation)                     \
  template(HeapIterateOperation)                  \
  template(ReportJavaOutOfMemory)                 \
  template(JFRCheckpoint)                         \
  template(ShenandoahFullGC)                      \
  template(ShenandoahInitMark)                    \
  template(ShenandoahFinalMarkStartEvac)          \
  template(ShenandoahInitUpdateRefs)              \
  template(ShenandoahFinalUpdateRefs)             \
  template(ShenandoahFinalRoots)                  \
  template(ShenandoahDegeneratedGC)               \
  template(Exit)                                  \
  template(LinuxDllLoad)                          \
  template(WhiteBoxOperation)                     \
  template(JVMCIResizeCounters)                   \
  template(ClassLoaderStatsOperation)             \
  template(ClassLoaderHierarchyOperation)         \
  template(DumpHashtable)                         \
  template(CleanClassLoaderDataMetaspaces)        \
  template(PrintCompileQueue)                     \
  template(PrintClassHierarchy)                   \
  template(PrintClasses)                          \
  template(ICBufferFull)                          \
  template(PrintMetadata)                         \
  template(GTestExecuteAtSafepoint)               \
  template(GTestStopSafepoint)                    \
  template(JFROldObject)                          \
  template(JvmtiPostObjectFree)                   \
  template(RendezvousGCThreads)

class VM_Operation : public StackObj {
 public:
  enum VMOp_Type {
    VM_OPS_DO(VM_OP_ENUM)
    VMOp_Terminating
  };

 private:
  Thread*         _calling_thread;

  // The VM operation name array
  static const char* _names[];

 public:
  VM_Operation() : _calling_thread(nullptr) {}

  // Called by VM thread - does in turn invoke doit(). Do not override this
  void evaluate();

  // evaluate() is called by the VMThread and in turn calls doit().
  // If the thread invoking VMThread::execute((VM_Operation*) is a JavaThread,
  // doit_prologue() is called in that thread before transferring control to
  // the VMThread.
  // If doit_prologue() returns true the VM operation will proceed, and
  // doit_epilogue() will be called by the JavaThread once the VM operation
  // completes. If doit_prologue() returns false the VM operation is cancelled.
  virtual void doit()                            = 0;
  virtual bool doit_prologue()                   { return true; };
  virtual void doit_epilogue()                   {};

  // An operation can either be done inside a safepoint
  // or concurrently with Java threads running.
  virtual bool evaluate_at_safepoint() const { return true; }

src/hotspot/share/runtime/vmThread.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void VMThread::loop() {
  assert(_cur_vm_operation == nullptr, "no current one should be executing");

  SafepointSynchronize::init(_vm_thread);

  // Need to set a calling thread for ops not passed
  // via the normal way.
  cleanup_op.set_calling_thread(_vm_thread);
  safepointALot_op.set_calling_thread(_vm_thread);

  while (true) {
    if (should_terminate()) break;
    wait_for_operation();
    if (should_terminate()) break;
    assert(_next_vm_operation != nullptr, "Must have one");
    inner_execute(_next_vm_operation);
  }
}

VM Operations 与 Safepoints 的关系

VMThread 会一直等待 VM Operation 出现在 VMOperationQueue 中,然后执行这些VM Operation。通常,这些操作需要虚拟机到达 Safepoint 后才能执行,因此会转给 VMThread。简单地说,当虚拟机处于 Safepoint 时,虚拟机内的所有线程都会被阻塞(blocked),并且在 Safepoint 期间,在执行 native code 的任何线程都无法返回到虚拟机。这意味着在执行 VM operation 时,不会有线程修改 Java 堆,而且所有线程都处于这样一种状态:它们的 Java stack 是不变的,可以被 GC 线程等检查。

大家最熟悉的 VM operation 是 GC,或者更具体地说是许多 GC 算法中常见的 “Stop The World ”阶段的 GC。但除了 GC 以外,还有许多其他基于 Safepoint 的 VM operation ,例如:有偏见的锁定撤销(biased locking revocation)、thread stack dumps、thread suspension 或 thread stopping(即 java.lang.Thread.stop() 方法)以及通过 JVMTI 请求的许多观察/修改操作。

许多 VM operation 是同步的,即请求者会阻塞直到操作完成,但也有一些是异步或并发的,即请求者可以与 VMThread 并行(当然,前提是没有启动 Safepoint )。

Safepoint 是通过一种基于轮询的合作机制发起的。简单来说,每隔一段时间就会有一个线程查询 “我是否应该在 Safepoint 阻塞 ?高效地完成这个查询并不简单。在线程状态转换过程中,就是经常查询的地方。一旦发起 Safepoint 请求,VMThread 必须等待所有线程都处于 Safepoint 安全状态后,才能继续执行 VM operation 。在 Safepoint 期间,Threads_lock 用于阻塞任何正在运行的线程,VMThread 最终会在 VM Operation 执行完毕后释放 Threads_lock

接收 Safepoint Request

可能是分配内存失败触发 GC,也可能是其它原因,Java 线程向 VM Thread 提出了进入 safepoint 的请求(VM_Operation),请求中带上 safepoint operation 参数,参数其实是 STOP THE WORLD(STW) 后要执行的 Callback 操作 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void VMThread::inner_execute(VM_Operation* op) {
...
  if (_cur_vm_operation->evaluate_at_safepoint() &&
      !SafepointSynchronize::is_at_safepoint()) {
    SafepointSynchronize::begin(); // <<<----
    if (has_timeout_task) {
      _timeout_task->arm(_cur_vm_operation->name());
    }
    end_safepoint = true;
  }

  evaluate_operation(_cur_vm_operation); // <<<----

  if (end_safepoint) {
    if (has_timeout_task) {
      _timeout_task->disarm();
    }
    SafepointSynchronize::end(); // <<<----
  }

...
}

(arming_safepoint)=

Arm Safepoint - 标记所有线程

VM Thread 线程在收到 safepoint request 后,修改一个 JVM 全局的 safepoint flag 为 true(这个 flag 可以是操作系统的内存页权限标识) 。

Arm Safepoint 术语中这个 arm 可以直译成 “武装/装备” ,但我翻译成设置标志

src/hotspot/share/runtime/safepoint.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Roll all threads forward to a safepoint and suspend them all
void SafepointSynchronize::begin() {
...
  int nof_threads = Threads::number_of_threads();
  _nof_threads_hit_polling_page = 0;
...
  // Arms the safepoint, _current_jni_active_count and _waiting_to_block must be set before.
  arm_safepoint();
  // Will spin until all threads are safe.
  int iterations = synchronize_threads(safepoint_limit_time, nof_threads, &initial_running);
  ...
}

void SafepointSynchronize::arm_safepoint() {
  // Begin the process of bringing the system to a safepoint.
  // Java threads can be in several different states and are
  // stopped by different mechanisms:
  //
  //  1. Running interpreted
  //     When executing branching/returning byte codes interpreter
  //     checks if the poll is armed, if so blocks in SS::block().
  //  2. Running in native code
  //     When returning from the native code, a Java thread must check
  //     the safepoint _state to see if we must block.  If the
  //     VM thread sees a Java thread in native, it does
  //     not wait for this thread to block.  The order of the memory
  //     writes and reads of both the safepoint state and the Java
  //     threads state is critical.  In order to guarantee that the
  //     memory writes are serialized with respect to each other,
  //     the VM thread issues a memory barrier instruction.
  //  3. Running compiled Code
  //     Compiled code reads the local polling page that
  //     is set to fault if we are trying to get to a safepoint.
  //  4. Blocked
  //     A thread which is blocked will not be allowed to return from the
  //     block condition until the safepoint operation is complete.
  //  5. In VM or Transitioning between states
  //     If a Java thread is currently running in the VM or transitioning
  //     between states, the safepointing code will poll the thread state
  //     until the thread blocks itself when it attempts transitions to a
  //     new state or locking a safepoint checked monitor.

  // We must never miss a thread with correct safepoint id, so we must make sure we arm
  // the wait barrier for the next safepoint id/counter.
  // Arming must be done after resetting _current_jni_active_count, _waiting_to_block.
...
  for (JavaThreadIteratorWithHandle jtiwh; JavaThread *cur = jtiwh.next(); ) {
    // Make sure the threads start polling, it is time to yield.
    SafepointMechanism::arm_local_poll(cur);
  }    

可见,vm thread 逐一 arm 所有的应用线程 。

自从 OpenJDK10 的 JEP 312: Thread-Local Handshakes - 2017年 后,就有了非 JVM Global 的 Safepoint - Thread Safepoint 。而 JVM Global 的 Safepoint 好像也修改为基于 Thread-Local Handshakes 去实现,即对每一条 JavaThread 执行 Thread-Local Handshakes

src/hotspot/share/runtime/safepointMechanism.inline.hpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void SafepointMechanism::arm_local_poll(JavaThread* thread) {
  thread->poll_data()->set_polling_word(_poll_word_armed_value);
  thread->poll_data()->set_polling_page(_poll_page_armed_value);
}

inline void SafepointMechanism::ThreadData::set_polling_word(uintptr_t poll_value) {
  Atomic::store(&_polling_word, poll_value);
}

inline void SafepointMechanism::ThreadData::set_polling_page(uintptr_t poll_value) {
  Atomic::store(&_polling_page, poll_value);
}

可以用下图说明 polling_page 的切换:

safepoint-switch-poll-page.png

图: polling_page 的切换. Source: The Inner Workings of Safepoints 2023 - mostlynerdless.de

等待应用线程到达 Safepoint

然后这个 VM Thread 就开始等待其它应用线程(App thread) 到达(进入) safepoint 。

src/hotspot/share/runtime/safepoint.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int SafepointSynchronize::synchronize_threads(jlong safepoint_limit_time, int nof_threads, int* initial_running)
{
  // Iterate through all threads until it has been determined how to stop them all at a safepoint.
  int still_running = nof_threads;
  ThreadSafepointState *tss_head = nullptr;
  ThreadSafepointState **p_prev = &tss_head;
  for (; JavaThread *cur = jtiwh.next(); ) {
    ThreadSafepointState *cur_tss = cur->safepoint_state();
    assert(cur_tss->get_next() == nullptr, "Must be null");
    if (thread_not_running(cur_tss)) {
      --still_running;
    } else {
      *p_prev = cur_tss;
      p_prev = cur_tss->next_ptr();
    }
  }
  ...

应用线程陷入 Safepoint

Java 线程会高频检查 safepoint flag(safepoint check/polling) ,当发现为 true(arm) 时,就到达(进入) safepoint 状态。

详见本书的 JavaThread Polling 与 Reach Safepoint - Reach and handle 一节。以下为摘录:

在 VMThread arm safepoint (详见本书的 Safepoint - Arm Safepoint - 标记所有线程) 后。polling 的应用线程最终会感知到 safepoint 的聚集要求(arming)。

  • 对于 绿色 immutable thread state 状态的 JavaThread:

    vm thread 通过 arm Java 线程的 polling page,这实际上在 arm safepoint 期间阻止了线程从所有绿色 immutable thread state 中唤醒/返回后,转换到任何红色 unsafe mutable thread state 。见 src/hotspot/share/utilities/globalDefinitions.hpp

    1
    2
    3
    
    // Each state has an associated xxxx_trans state, which is an intermediate state used when a thread is in
    // a transition from one state to another. These extra states makes it possible for the safepoint code to
    // handle certain thread_states without having to suspend the thread - making the safepoint code faster.
    

HotSpot JVM Deep Dive - Safepoint 19-43 screenshot

图: 当 JavaThread 被arm polling page 后的状态机变化 。Source: HotSpot JVM Deep Dive - Safepoint

  • 对于 红色 mutable thread state 状态的 JavaThread:

    vm thread 通过 arm Java 线程的 polling page, 触发 Java 线程从 mutable thread state 转换为 immutable thread state 状态。并且作为此转换的结果,线程本地 GC 树被同步到 JavaThread 对象。

    对于 VM state 的线程,这意味着需要等待线程自行完成转换。VM state 中只有少数几个地方显式执行安全点检查。例如,在争夺 VM mutex 互斥锁VM monitor 时。此设计的前提是 Java 线程应尽可能少地处于 VM state。但对于在 state java 下运行的线程,情况有所不同。

下图举一个例子,尝试说明在几种线程状态和操作系统调度环境下,线程到达 Safepoint (GetStackTrace 需要 Stop The World) 的情况。

SafepointOverheads.png

图: 几种状态和系统调度环境下,线程到达 Safepoint 的情况. Source: Safepoints: Meaning, Side Effects and Overheads - psy-lob-saw.blogspot.com

  • 绿色箭头:java state thread and running on CPU
  • 黄色箭头:java state thread and off CPU (因 CPU 资源不足等原因)
  • 红色箭头:JNI state thread

从 VMThread arm safepoint 到 应用线程 Reach Safepoint 的延迟,叫 Time To Safe Point(TTSP)

每个线程在进行 safepoint check 时如发现 safepoint arming 都会进入安全点。但到达 safepoint check 前需要执行机器指令的数量不是固定的。上图中,我们可以看到:

  • J1 直接命中安全点轮询并被暂停。J2 和 J3 正在争夺可用的 CPU 时间。J3 抢占了一些 CPU 时间,将 J2 推入运行队列,但 J2 并未进入安全点。J3 到达安全点并暂停,从而腾出内核,让 J2 取得足够的进展,进入安全点轮询。

  • J4 和 J5 在执行 JNI 代码(JNI state)时属于Immutable thread state,它们不受 Safepoint 挂起影响。请注意,J5 在 Stop The World 执行到一半时试图离开 JNI,并在恢复执行 Java 代码前被暂停。重要的是,我们观察到不同线程到达安全点的时间各不相同,有些线程暂停的时间比其他线程长,Java 线程花很长时间到达安全点可能会耽误其他线程。

OpenJDK9 前,用 -XX:+PrintGCApplicationStoppedTime 可以打印出 TTSP 。OpenJDK9 后,由于采用了 Unified Logging for GC logging 的设计,配置修改成:
-Xlog:safepoint

Signal Handle

JVM 启动初始化时,安装了JVM 自用的 Signal Handler :

Stack :

libjvm.so!PosixSignals::install_sigaction_signal_handler(sigaction * sigAct, sigaction * oldSigAct, int sig, sa_sigaction_t handler) (/jdk/src/hotspot/os/posix/signals_posix.cpp:900)
libjvm.so!set_signal_handler(int sig) (/jdk/src/hotspot/os/posix/signals_posix.cpp:1271)
libjvm.so!install_signal_handlers() (/jdk/src/hotspot/os/posix/signals_posix.cpp:1313)
libjvm.so!PosixSignals::init() (/jdk/src/hotspot/os/posix/signals_posix.cpp:1855) // <<<<----
libjvm.so!os::init_2() (/jdk/src/hotspot/os/linux/os_linux.cpp:4613)
libjvm.so!Threads::create_vm(JavaVMInitArgs * args, bool * canTryAgain) (/jdk/src/hotspot/share/runtime/threads.cpp:482)
libjvm.so!JNI_CreateJavaVM_inner(JavaVM ** vm, void ** penv, void * args) (/jdk/src/hotspot/share/prims/jni.cpp:3577)
libjvm.so!JNI_CreateJavaVM(JavaVM ** vm, void ** penv, void * args) (/jdk/src/hotspot/share/prims/jni.cpp:3668)
libjli.so!InitializeJVM(JavaVM ** pvm, JNIEnv ** penv, InvocationFunctions * ifn) (/jdk/src/java.base/share/native/libjli/java.c:1506)
libjli.so!JavaMain(void * _args) (/jdk/src/java.base/share/native/libjli/java.c:415)
libjli.so!ThreadJavaMain(void * args) (/jdk/src/java.base/unix/native/libjli/java_md.c:650)
libc.so.6!start_thread(void * arg) (pthread_create.c:442)
libc.so.6!clone3() (clone3.S:81)

src/hotspot/os/posix/signals_posix.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// install signal handlers for signals that HotSpot needs to
// handle in order to support Java-level exception handling.
void install_signal_handlers() {
...    
  set_signal_handler(SIGSEGV); // <<<<<<<
...
}

void set_signal_handler(int sig) {
...
  struct sigaction sigAct;
  int ret = PosixSignals::install_sigaction_signal_handler(&sigAct, &oldAct,
                                                           sig, javaSignalHandler); // <<<<----
}

// Entry point for the hotspot signal handler.
static void javaSignalHandler(int sig, siginfo_t* info, void* context) {
  // Do not add any code here!
  // Only add code to either JVM_HANDLE_XXX_SIGNAL or PosixSignals::pd_hotspot_signal_handler.
  (void)JVM_HANDLE_XXX_SIGNAL(sig, info, context, true);
}

int JVM_HANDLE_XXX_SIGNAL(int sig, siginfo_t* info,
                          void* ucVoid, int abort_if_unrecognized)
{
  // Call platform dependent signal handler.
  if (!signal_was_handled) {
    JavaThread* const jt = (t != nullptr && t->is_Java_thread()) ? JavaThread::cast(t) : nullptr;
    signal_was_handled = PosixSignals::pd_hotspot_signal_handler(sig, info, uc, jt); // <<<<----
  }
...
}

Signal Handler 实现:

src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
bool PosixSignals::pd_hotspot_signal_handler(int sig, siginfo_t* info,
                                             ucontext_t* uc, JavaThread* thread) {
...
    if (thread->thread_state() == _thread_in_Java) {
      // Java thread running in Java code => find exception handler if any
      // a fault inside compiled code, the interpreter, or a stub

      if (sig == SIGSEGV && SafepointMechanism::is_poll_address((address)info->si_addr)) {
        stub = SharedRuntime::get_poll_stub(pc); //  <<<<----
      } else if (sig == SIGBUS /* && info->si_code == BUS_OBJERR */) {..}
      ...
      } else if (sig == SIGSEGV &&
                 MacroAssembler::uses_implicit_null_check(info->si_addr)) {
          // Determination of interpreter/vtable stub/compiled code null exception
          stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL);
      }
    } else if ((thread->thread_state() == _thread_in_vm ||
                thread->thread_state() == _thread_in_native) &&
               (sig == SIGBUS && /* info->si_code == BUS_OBJERR && */
               thread->doing_unsafe_access())) {
        address next_pc = Assembler::locate_next_instruction(pc);
        if (UnsafeCopyMemory::contains_pc(pc)) {
          next_pc = UnsafeCopyMemory::page_error_continue_pc(pc);
        }
        stub = SharedRuntime::handle_unsafe_access(thread, next_pc);
    }
    // jni_fast_Get<Primitive>Field can trap at certain pc's if a GC kicks in
    // and the heap gets shrunk before the field access.
    if ((sig == SIGSEGV) || (sig == SIGBUS)) {
      address addr = JNI_FastGetField::find_slowcase_pc(pc);
      if (addr != (address)-1) {
        stub = addr;
      }
    }
...
  if (stub != nullptr) {
    // save all thread context in case we need to restore it
    if (thread != nullptr) thread->set_saved_exception_pc(pc);

    os::Posix::ucontext_set_pc(uc, stub); // <<<<---- 直接修改 PC 寄存器,goto 跳到上面获取的 SafepointBlob Stub 机器码中
    return true;
  }

}

SafepointBlob Stub 机器码获取 - poll_stub

src/hotspot/share/runtime/sharedRuntime.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
address SharedRuntime::get_poll_stub(address pc) {
  address stub;
  // Look up the code blob
  CodeBlob *cb = CodeCache::find_blob(pc);

  // Should be an nmethod
  guarantee(cb != nullptr && cb->is_compiled(), "safepoint polling: pc must refer to an nmethod");

  // Look up the relocation information
  assert(((CompiledMethod*)cb)->is_at_poll_or_poll_return(pc),
      "safepoint polling: type must be poll at pc " INTPTR_FORMAT, p2i(pc));
...
  bool at_poll_return = ((CompiledMethod*)cb)->is_at_poll_return(pc);
  bool has_wide_vectors = ((CompiledMethod*)cb)->has_wide_vectors();
  if (at_poll_return) {
    assert(SharedRuntime::polling_page_return_handler_blob() != nullptr,
           "polling page return stub not created yet");
    stub = SharedRuntime::polling_page_return_handler_blob()->entry_point();
  } else if (has_wide_vectors) {
    assert(SharedRuntime::polling_page_vectors_safepoint_handler_blob() != nullptr,
           "polling page vectors safepoint stub not created yet");
    stub = SharedRuntime::polling_page_vectors_safepoint_handler_blob()->entry_point();
  } else {
    assert(SharedRuntime::polling_page_safepoint_handler_blob() != nullptr,
           "polling page safepoint stub not created yet");
    stub = SharedRuntime::polling_page_safepoint_handler_blob()->entry_point(); // <<<<<<<
  }
  log_debug(safepoint)("... found polling page %s exception at pc = "
                       INTPTR_FORMAT ", stub =" INTPTR_FORMAT,
                       at_poll_return ? "return" : "loop",
                       (intptr_t)pc, (intptr_t)stub);
  return stub;
}

// JVM 启动初始化时,SafepointBlob Stub 机器码生成
static  _polling_page_safepoint_handler_blob = generate_handler_blob(CAST_FROM_FN_PTR(address, SafepointSynchronize::handle_polling_page_exception), POLL_AT_LOOP); // <<<<<<<<<
static  _polling_page_return_handler_blob    = generate_handler_blob(CAST_FROM_FN_PTR(address, SafepointSynchronize::handle_polling_page_exception), POLL_AT_RETURN);

JVM 启动初始化时,SafepointBlob Stub 机器码生成:

src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//------------------------------generate_handler_blob------
//
// Generate a special Compile2Runtime blob that saves all registers,
// and setup oopmap.
//
SafepointBlob* SharedRuntime::generate_handler_blob(address call_ptr, int poll_type) {
  assert(StubRoutines::forward_exception_entry() != nullptr,
         "must be generated before");

  ResourceMark rm;
  OopMapSet *oop_maps = new OopMapSet();
  OopMap* map;

  // Allocate space for the code.  Setup code generation tools.
  CodeBuffer buffer("handler_blob", 2048, 1024);
  MacroAssembler* masm = new MacroAssembler(&buffer);

  address start   = __ pc();
  address call_pc = nullptr;
  int frame_size_in_words;
  bool cause_return = (poll_type == POLL_AT_RETURN);
  bool save_wide_vectors = (poll_type == POLL_AT_VECTOR_LOOP);

  if (UseRTMLocking) {
    // Abort RTM transaction before calling runtime
    // because critical section will be large and will be
    // aborted anyway. Also nmethod could be deoptimized.
    __ xabort(0);
  }

  // Make room for return address (or push it again)
  if (!cause_return) {
    __ push(rbx);
  }

  // Save registers, fpu state, and flags
  map = RegisterSaver::save_live_registers(masm, 0, &frame_size_in_words, save_wide_vectors);

  // The following is basically a call_VM.  However, we need the precise
  // address of the call in order to generate an oopmap. Hence, we do all the
  // work ourselves.

  __ set_last_Java_frame(noreg, noreg, nullptr, rscratch1);  // JavaFrameAnchor::capture_last_Java_pc() will get the pc from the return address, which we store next:

  // The return address must always be correct so that frame constructor never
  // sees an invalid pc.

  if (!cause_return) {
    // Get the return pc saved by the signal handler and stash it in its appropriate place on the stack.
    // Additionally, rbx is a callee saved register and we can look at it later to determine
    // if someone changed the return address for us!
    __ movptr(rbx, Address(r15_thread, JavaThread::saved_exception_pc_offset()));
    __ movptr(Address(rbp, wordSize), rbx);
  }

  // Do the call
  __ mov(c_rarg0, r15_thread);
  __ call(RuntimeAddress(call_ptr));

  // Set an oopmap for the call site.  This oopmap will map all
  // oop-registers and debug-info registers as callee-saved.  This
  // will allow deoptimization at this safepoint to find all possible
  // debug-info recordings, as well as let GC find all oops.

  oop_maps->add_gc_map( __ pc() - start, map);

  Label noException;

  __ reset_last_Java_frame(false);

  __ cmpptr(Address(r15_thread, Thread::pending_exception_offset()), NULL_WORD);
  __ jcc(Assembler::equal, noException);

  // Exception pending

  RegisterSaver::restore_live_registers(masm, save_wide_vectors);

  __ jump(RuntimeAddress(StubRoutines::forward_exception_entry()));

  // No exception case
  __ bind(noException);

  Label no_adjust;
...
  if (!cause_return) {
    Label no_prefix, not_special;

    // If our stashed return pc was modified by the runtime we avoid touching it
    __ cmpptr(rbx, Address(rbp, wordSize));
    __ jccb(Assembler::notEqual, no_adjust);

    // Skip over the poll instruction.
    // See NativeInstruction::is_safepoint_poll()
    // Possible encodings:
    //      85 00       test   %eax,(%rax)
    //      85 01       test   %eax,(%rcx)
    //      85 02       test   %eax,(%rdx)
    //      85 03       test   %eax,(%rbx)
    //      85 06       test   %eax,(%rsi)
    //      85 07       test   %eax,(%rdi)
    //
    //   41 85 00       test   %eax,(%r8)
    //   41 85 01       test   %eax,(%r9)
    //   41 85 02       test   %eax,(%r10)
    //   41 85 03       test   %eax,(%r11)
    //   41 85 06       test   %eax,(%r14)
    //   41 85 07       test   %eax,(%r15)
    //
    //      85 04 24    test   %eax,(%rsp)
    //   41 85 04 24    test   %eax,(%r12)
    //      85 45 00    test   %eax,0x0(%rbp)
    //   41 85 45 00    test   %eax,0x0(%r13)

    __ cmpb(Address(rbx, 0), NativeTstRegMem::instruction_rex_b_prefix);
    __ jcc(Assembler::notEqual, no_prefix);
    __ addptr(rbx, 1);
    __ bind(no_prefix);
...
    // r12/r13/rsp/rbp base encoding takes 3 bytes with the following register values:
    // r12/rsp 0x04
    // r13/rbp 0x05
    __ movzbq(rcx, Address(rbx, 1));
    __ andptr(rcx, 0x07); // looking for 0x04 .. 0x05
    __ subptr(rcx, 4);    // looking for 0x00 .. 0x01
    __ cmpptr(rcx, 1);
    __ jcc(Assembler::above, not_special);
    __ addptr(rbx, 1);
    __ bind(not_special);
...
    // Adjust return pc forward to step over the safepoint poll instruction
    __ addptr(rbx, 2);
    __ movptr(Address(rbp, wordSize), rbx);
  }

  __ bind(no_adjust);
  // Normal exit, restore registers and exit.
  RegisterSaver::restore_live_registers(masm, save_wide_vectors);
  __ ret(0);

...

  // Make sure all code is generated
  masm->flush();

  // Fill-out other meta info
  return SafepointBlob::create(&buffer, oop_maps, frame_size_in_words);
}

Global safepoint - The World Stopped

VM Thread 发现所有 App thread 都到达 safepoint (真实的 STW 的开始) 。就开始执行 safepoint operationGC 操作safepoint operation 其中一种可能类型。

源码 RuntimeService::record_safepoint_synchronized()

Safepoint operation 结束

safepoint operation 执行完毕, VM Thread 结束 STW 。

源码 SafepointSynchronize::end()

Disarming Safepoint

src/hotspot/share/runtime/safepointMechanism.inline.hpp

1
2
3
4
5
// Disarming one thread 
void SafepointMechanism::disarm_local_poll(JavaThread* thread) {
  thread->poll_data()->set_polling_word(_poll_word_disarmed_value);
  thread->poll_data()->set_polling_page(_poll_page_disarmed_value);
}

Safepoint 问题排查

Safepoint 是 JVM 性能问题的热点爆发地。我之前写有一些文章去排查相关问题:

参考

分享

Mark Zhu
作者
Mark Zhu
An old developer