# JVM Hotspot GC 如何 STW 原理分析

线程安全点介绍

读者可以考虑两个问题:

  1. 在JVM垃圾回收时,需要STW,我在什么时候STW?
  2. 在JVM垃圾回收时,需要初始 GC root 根对象标记,那么我们知道有一个地方是可以作为根对象的:线程栈内存,那么我如何知道栈内存中哪些位置存放着oop对象指针呢?

这两个问题是可以一起来回答的,我们先来看第二个问题,我们需要在哪些位置保存栈内存的oop指针信息呢?是随便保存,还是有指定位置?那么如果我们不保存呢?我们先考虑如果不保存这些信息,那么在GC时,我就需要将线程栈挨个地址遍历,找到对象指针oop,这是不现实的,因为这会导致性能极度下降。那好吧,我们只能记录这些信息了,而在Hotspot中使用OopMap类来记录这些信息(其实是OopMapSet 类 + OopMap 类,因为一个OopMap 表示一个位置,我打算下次在另一篇文章中详细写关于这两个类的原理,读者这里先大致了解下即可)。那好此时我们可以回答第一个问题了,我们可以让线程在 OopMap 处停止,而这些OopMap 放置的地方称之为线程安全点。当所有线程都处于线程安全点了,那么我们称之为STW。现在我们只需要解决一个问题即可:在哪些地方放置OopMap ,换句话说哪些地方是线程安全点:

  1. 循环体的结尾
  2. 方法返回前
  3. 调用方法的call之后
  4. 抛出异常的位置

SafepointSynchronize类原理

SafepointSynchronize类用于实现线程安全点的所有功能,比如将让所有线程进入线程安全点,或者让所有线程从安全点恢复。

SafepointSynchronize::begin方法原理

该方法用于让所有线程进入线程安全点,并且阻塞他们。我们看到,该方法只能由VM_thread来执行(VM_thread是Hotspot中的处理重量级任务的线程,该线程只有一个,而GC操作也是由该线程完成,因为是单线程,所以我们递交到它队列里完成时,是线程安全的)。我们首先停止GC处理线程,随后获取线程锁Threads_lock,此时不允许再往系统中创建和销毁线程,同时保存Java 活动的java线程计数,然后我们开始让整个系统进入线程安全点并暂停线程,我们需要注意的是,此时Java线程可能在以下几种不同的处理状态中,对这些不同的状态我们要对其进行相应的处理:

  1. 正在解释执行字节码的线程
  2. 此时不需要进行特殊处理,因为我们会在OopMap的放置处进行线程安全点检测,如果我们设置了线程安全点,那么线程会在解释执行的过程中检测到标志位,那么自行停止即可
  3. 正在执行JNI Native的线程
  4. 当线程在执行JNI代码时,此时与Java系统无关,所以我们只需要让JNI的代码在访问Java系统时检测以下线程安全点状态state即可
  5. 正在执行JIT编译后字节码的线程
  6. 当字节码被JIT编译后,将会在编译代码中插入检测线程安全点的代码,当其访问该代码时将会进入阻塞状态
  7. 处于阻塞状态的线程
  8. 处于阻塞状态的线程本身就是线程安全的,所以我们只需要在唤醒线程之前检测线程安全点即可
  9. 正在处于VM虚拟机操作或者转变线程状态的线程
  10. 当线程处于VM操作状态或者转变线程状态时,那么将由线程在转变为新的状态时,检测安全点状态,然后自身进入阻塞状态
void SafepointSynchronize::begin() {

 Thread* myThread = Thread::current();

 assert(myThread->is_VM_thread(), "Only VM thread may execute a safepoint");

 #if INCLUDE_ALL_GCS

 if (UseConcMarkSweepGC) {

  // 使用CMS回收器,那么暂停CMS线程

  ConcurrentMarkSweepThread::synchronize(false);

 } else if (UseG1GC) {

  // 使用G1回收器,那么也需要通知暂停GC线程

  ConcurrentGCThread::safepoint_synchronize();

 }

 #endif 

 // 获取线程锁,此时不允许再创建和销毁线程

 Threads_lock->lock();

 // 获取所有Java 活动的java线程计数

 int nof_threads = Threads::number_of_threads();

 ...

 // 获取线程安全点锁,注意:这把锁为互斥锁,如果其他线程再次获取,那么需要阻塞,这里很关键

 MutexLocker mu(Safepoint_lock);

 // 重置JNI活动线程

 _current_jni_active_count = 0;

 _waiting_to_block = nof_threads;

 TryingToBlock = 0 ;

 int still_running = nof_threads;

 // state变量用于表示线程安全点状态,我们看到首先将状态修改为 _synchronizing 状态

 _state   = _synchronizing;

 OrderAccess::fence(); // 使用系统屏障保证不会发生重排序,且保证_state状态的修改被其他线程所看见

 if (!UseMembar) { // 如果我们不使用全屏障指令,那么我们调用serialize_thread_states方法来序列化线程状态的读写操作,这个我们会在后面详细讲解,这里我们只需要知道是:为了保证线程从JNI方法执行转到执行字节码时,保证VMThread读取到最新的线程状态

  os::serialize_thread_states();

 }

 // 通知解释器当前已经进入线程安全点

 Interpreter::notice_safepoints();

 // 将polling page 轮询页 设置为不可访问状态

 if (UseCompilerSafepoints && DeferPollingPageLoopCount < 0) {

  PageArmed = 1 ;

  os::make_polling_page_unreadable();

 }

 // 获取cpu核心数

 int ncpus = os::processor_count() ;

 // 如果设置线程安全点超时检测输出debug信息,那么计算超时时间

 if (SafepointTimeout)

  safepoint_limit_time = os::javaTimeNanos() + (jlong)SafepointTimeoutDelay * MICROUNITS;

 // 遍历线程链表,等待他们都进入线程安全点

 unsigned int iterations = 0;

 int steps = 0 ;

 while(still_running > 0) {

  for (JavaThread *cur = Threads::first(); cur != NULL; cur = cur->next()) {

   // 获取当前Java线程运行状态

   ThreadSafepointState *cur_state = cur->safepoint_state();

   if (cur_state->is_running()) { // 如果Java线程处于运行状态,那么调用examine_state_of_thread方法帮助线程滚动到线程安全点

    cur_state->examine_state_of_thread();

    if (!cur_state->is_running()) { // 线程成功停止,那么减少活动线程计数

     still_running--;

    }

   }

   ... // 此处省略掉超时检测和避免CPU空转的优化机制

  }

 }

 assert(still_running == 0, "sanity check"); // 此时必须所有Java线程都进入了线程安全点且状态不为_running状态



 // 如果仍然有线程没有进入阻塞状态,那么需要等待他们都进入安全点,我们虽然在上面的循环中改变了线程的状态不为_running,但是,有可能线程只是修改了状态还没有阻塞,此时需要等待

 while (_waiting_to_block > 0) {

  Safepoint_lock->wait(true); 

 }

 assert(_waiting_to_block == 0, "sanity check"); // 所有Java线程必须都已经处于线程安全点状态

 _safepoint_counter ++; // 线程安全点计数++

 // 修改状态为_synchronized表示已经将所有线程进入线程安全点,当前只有VMThread当前线程执行

 _state = _synchronized;

 OrderAccess::fence(); // 指令全屏障保证以上修改的可见性

 ...

}
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

线程安全点状态与线程状态原理

SynchronizeState枚举类用于描述SafepointSynchronize同步类的安全点状态。它分为三个状态。详细描述如下。



enum SynchronizeState {

 _not_synchronized = 0,     // 系统没有开启线程安全点同步

 _synchronizing = 1,     // 系统开启线程安全点同步,但此时还在等待线程进入block状态

 _synchronized = 2     // 所有线程已经停止执行,只有VMThread在执行,此时表明完全进入安全点状态

};
1
2
3
4
5
6
7
8
9
10
11

suspend_type枚举用于表示线程在处理线程安全点时的状态。同样由三个类型。详细描述如下。



enum suspend_type {

 _running    = 0, // 线程处于运行状态,并没有进入线程安全点

 _at_safepoint   = 1, // 线程已经进入线程安全点,比如进入了block状态

 _call_back    = 2 // 如果线程在解释执行或vm状态中,保持执行并等待回调

};
1
2
3
4
5
6
7
8
9
10
11

JavaThreadState枚举类用于表示线程目前在执行代码的状态。我们看到,为了支持状态的转换期间的操作我们对每一个状态后面都增加了一个过渡状态_trans结尾的状态,表明从上一个状态转变到下一个状态的中间状态。我们主要关心四个重要的状态描述即可。

  1. _thread_new : 新建状态,还没有被执行
  2. thread_in_native : 正在执行JNI代码
  3. _thread_in_vm : 正在VM中执行非用户代码
  4. _thread_in_Java : 正在解释执行字节码或者执行JIT编译的本地代码
enum JavaThreadState {

 _thread_uninitialized = 0, // 枚举类的初始值,不可能检测到该变量,除非代码出现了bug

 _thread_new    = 2,

 _thread_new_trans  = 3, 

 _thread_in_native  = 4, 

 _thread_in_native_trans = 5, 

 _thread_in_vm   = 6, 

 _thread_in_vm_trans  = 7,

 _thread_in_Java   = 8,

 _thread_in_Java_trans = 9, 

 _thread_blocked   = 10, // 阻塞状态

 _thread_blocked_trans = 11, 

 _thread_max_state  = 12 // 最大状态,由于统计分析使用

};
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

serialize_thread_states方法原理

该方法用于序列化线程状态的修改和访问,用于实现fence全屏障的优化。如果直接讲该方法的话可能有点唐突,毕竟我们知道该方法是针对线程状态变化的,那么我们先来看JNI方法返回后需要对线程状态进行修改且为了避免越过线程安全点继续执行java代码,导致系统出现问题,那么需要对这些状态访问进行序列化。我们来看transition_from_native方法,该方法当线程从执行JNI代码中返回时调用。

我们看到该方法首先将状态变为_thread_in_native_trans,然后根据UseMembar变量来选择使用fence屏障指令或者serialize_thread_states方法来完成线程状态修改对VMthread线程的可见性还有禁止指令重排序,而我们通常不使用UseMembar,那么这时使用serialize_memory来取代fence全屏障的操作,当该方法调用完成后,那么线程状态将会对VMThread可见,最后我们在设置线程最终状态时,检测当前系统是否处于线程安全点,如果在该状态,那么需要阻塞线程。详细实现如下。

static inline void transition_from_native(JavaThread *thread, JavaThreadState to) {

 // 转变线程状态为_thread_in_native_trans,表明正从JNI返回执行Java代码

 thread->set_thread_state(_thread_in_native_trans);

 // 如果是多处理,那么根据UseMembar变量来选择使用fence屏障指令或者serialize_thread_states方法来完成线程状态修改对VMthread线程的可见性还有禁止指令重排序

 if (os::is_MP()) {

  if (UseMembar) {

   OrderAccess::fence();

  } else {

   // 由于Windows不同于Linux,所以需要调用该接口方法,不过我们都是研究Linux,所以该方法直接调用write_memory_serialize_page方法

   // static inline void serialize_memory(JavaThread *thread) {

    //  os::write_memory_serialize_page(thread);

   // }

   InterfaceSupport::serialize_memory(thread);

  }

 }

 // 检测当前系统处于线程安全点,如果处于该状态,那么调用check_safepoint_and_suspend_for_native_trans阻塞当前线程

 if (SafepointSynchronize::do_call_back() || thread->is_suspend_after_native()) {

  JavaThread::check_safepoint_and_suspend_for_native_trans(thread);

 }

 // 一切正常,那么设置线程状态为想要修改的状态,通常该状态为_thread_in_Java

 thread->set_thread_state(to);

}
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

接下来我们来看os::write_memory_serialize_page方法的实现。我们看到首先根据JavaThread地址计算出_mem_serialize_page中的偏移量,然后向该偏移量处写入1。那么可能读者会问两个问题:

  1. 为啥要计算偏移量?
  2. 我们知道缓存行大小通常为64byte,而4kb=4096byte,我们为了避免多个线程之间写入这个值导致缓存行共享问题,导致性能下降所以进行偏移量计算让不同线程尽量写不同缓存行
  3. 为何写1,写其他值行不行,这么做是为何?
  4. 写1写0写其他值都不重要,这里是为了搭配serialize_thread_states方法使用的,我们记住这里有一个写入操作即可。
static inline void write_memory_serialize_page(JavaThread *thread) {

 uintptr_t page_offset = ((uintptr_t)thread >>

       get_serialize_page_shift_count()) & get_serialize_page_mask();

 *(volatile int32_t *)((uintptr_t)_mem_serialize_page+page_offset) = 1;

}
1
2
3
4
5
6
7
8
9

接下来我们来看SafepointSynchronize::do_call_back() 方法和JavaThread::check_safepoint_and_suspend_for_native_trans(thread)方法的实现原理。我们看到do_call_back其实就是检测当前系统是否进入了线程安全点,如果判断处于安全点,那么就需要阻塞当前线程对象。详细实现如下。

// 查看安全点状态是否为_not_synchronized状态,如果不是,那么表明当前系统已经开始STW

inline static bool do_call_back() {

 return (_state != _not_synchronized);

}



// 如果系统处于线程安全点状态,那么我们需要阻塞当前线程

void JavaThread::check_safepoint_and_suspend_for_native_trans(JavaThread *thread) {

 JavaThread *curJT = JavaThread::current(); // 获取当前执行改代码的JavaThread对象指针

 ...

 if (SafepointSynchronize::do_call_back()) { // 再次检测线程安全点状态

  // 如果处于线程安全点,那么阻塞当前线程

  SafepointSynchronize::block(curJT);

 }

 ...

}
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

我们继续跟进SafepointSynchronize::block(curJT)方法。我们看到该方法根据线程状态来选择不同的执行过程,我们看到如果线程正在执行字节码或者从vm状态转换期间调用了该方法,那么我们需要减少waiting_to_block计数,同时负责唤醒在begin方法中等待所有方法阻塞的VMThread线程,此时表明所有线程均阻塞(STW),随后将自己设置为阻塞状态,然后通过获取Threads_lock锁来让自己阻塞,因为我们知道VMThread已经获取到了该锁,所以当前线程获取锁必定会阻塞。而对于从JNI方法中返回的线程而言,只需要改变状态为thread_blocked然后阻塞即可。详细实现如下。

void SafepointSynchronize::block(JavaThread *thread) {

 ...

 switch(state) { // 根据当前线程状态来选择如何阻塞

 case _thread_in_vm_trans:

 case _thread_in_Java:  // 当前线程状态为vm转换状态或者执行字节码状态

  thread->set_thread_state(_thread_in_vm); // 首先将状态转为_thread_in_vm

  if (is_synchronizing()) { // 如果当前VMThread正处于等待所有线程进入线程安全点状态,那么增加TryingToBlock计数

  Atomic::inc (&TryingToBlock) ;

 }

  Safepoint_lock->lock_without_safepoint_check(); // 获取安全点锁

  // 再次判断是否在等待线程阻塞,如果是,那么减少等待计数,同时将线程线程安全点状态为回调执行设置为true,这里用于调试我们忽略它的作用即可。接着如果线程正在执行关键的JNI代码,那么此时增加JNI活动计数,最后如果thread为最后一个需要等待阻塞的线程,那么唤醒阻塞在Safepoint_lock等待所有线程阻塞的VMThread线程

  if (is_synchronizing()) {

  _waiting_to_block--;

  thread->safepoint_state()->set_has_called_back(true);

  if (thread->in_critical()) {

   increment_jni_active_count();

  }

  if (_waiting_to_block == 0) {

   Safepoint_lock->notify_all();

  }

 }

  // 设置thread状态为_thread_blocked,同时释放Safepoint_lock锁

  thread->set_thread_state(_thread_blocked);

  Safepoint_lock->unlock();

  // 此时如果VMThread获取了Safepoint_lock并开始执行STW的操作,那么由于VMthread线程获取到了线程锁Threads_lock,所以此时当前Thread会被阻塞

  Threads_lock->lock_without_safepoint_check();

  thread->set_thread_state(state); // 当从阻塞中唤醒后,说明此时已经过了线程安全点,那么将线程状态恢复

  Threads_lock->unlock();

  break;



 // 线程处于以下三个状态,那么直接设置状态为_thread_blocked,同时获取Threads_lock阻塞,直到VMThread释放该锁

 case _thread_in_native_trans:

 case _thread_blocked_trans:

 case _thread_new_trans:

  thread->set_thread_state(_thread_blocked);

  Threads_lock->lock_without_safepoint_check();

  thread->set_thread_state(state);

  Threads_lock->unlock();

  break;



 default:

 fatal(err_msg("Illegal threadstate encountered: %d", state));

 }

 ...

}
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

那么此时我们是不是就可以来看看serialize_thread_states方法为什么要这么做了。在write_memory_serialize_page方法中我们知道让线程往这个memory_serialize_page页写入了1,而这里却对该页首先设置为MEM_PROT_READ,表明该页只读,然后再次修改为MEM_PROT_RW表示可读写,这是为何?我们知道在Linux中如果一个页被设置为了只读,如果你往其中写入数据,那么将会发生SIGSEGV信号,而该信号将会被JVM进程所设置的signal handler处理,同时必须要注意的是:当页面权限被修改后将会强制CPU的store buffer进行刷新,从而导致其中写入的变量被其他CPU可见,这很重要,利用了该特性完成了类似fence全屏障的操作。那么这里就差最后一个问题没有解决了:为啥要获取SerializePageLock页面锁?我们看block_on_serialize_page_trap方法,该方法作为SIGSEGV信号的处理方法,我们看到在其中获取到了SerializePageLock,考虑下如果VMThread在对memory_serialize_page页进行访问权限操作期间,由于刚设置了MEM_PROT_READ,而其他线程对该页进行了写1操作,此时就会发生SIGSEGV信号给当前线程来处理该信号,而此时便让线程等待VMThread将权限修改为MEM_PROT_RW即可。而我们可不可以不用该锁了,貌似没有什么实际意义?是这样的,在Linux有些平台上修改页权限,由于进程调度器的问题将会导致修改的权限可能延迟一段时间才能被其他线程可见,而为了避免这段时间导致其他线程一直接收到SIGSEGV调用block_on_serialize_page_trap方法,那么此时我们通过锁机制来完成该操作。详细实现如下。

void os::block_on_serialize_page_trap() {

 Thread::muxAcquire(&SerializePageLock, "set_memory_serialize_page");

 Thread::muxRelease(&SerializePageLock);

}



void os::serialize_thread_states() {

 Thread::muxAcquire(&SerializePageLock, "serialize_thread_states");

 os::protect_memory((char *)os::get_memory_serialize_page(),

      os::vm_page_size(), MEM_PROT_READ); // 修改页只读

 os::protect_memory((char *)os::get_memory_serialize_page(), 

      os::vm_page_size(), MEM_PROT_RW); // 修改页可读写

 Thread::muxRelease(&SerializePageLock);

}
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

examine_state_of_thread方法原理

接下来我们来看examine_state_of_thread方法,我们知道该方法将会决定线程以什么样的方式进入阻塞状态。首先我们看看当前线程是否已经被外部线程挂起,如果是,那么调用roll_forward将线程推动到线程安全点,

void ThreadSafepointState::examine_state_of_thread() {

 JavaThreadState state = _thread->thread_state();

 _orig_thread_state = state;

 // 当线程恢复是需要持有线程锁,由于该锁已经被VMTHREAD获取,所以这里没有任何问题

 bool is_suspended = _thread->is_ext_suspended();

 if (is_suspended) {

 roll_forward(_at_safepoint);

 return;

 }

 // 如果线程此时已经处于线程安全点也即:_thread_in_native或者_thread_blocked,那么将其也滚动到线程安全点

 if (SafepointSynchronize::safepoint_safe(_thread, state)) {

 SafepointSynchronize::check_for_lazy_critical_native(_thread, state);

 roll_forward(_at_safepoint);

 return;

 }

 // 处于thread_in_vm状态,那么滚动状态为_call_back

 if (state == _thread_in_vm) {

 roll_forward(_call_back);

 return;

 }

 // 如果是其他状态的线程,那么让他们自己进入_thread_blocked状态,在下一次循环进入该方法时将会在SafepointSynchronize::safepoint_safe(_thread, state)判断处理

 assert(is_running(), "examine_state_of_thread on non-running thread");

 return;

}
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

接下来我们来看roll_forward方法的原理,该方法用于将线程滚动到线程安全点。我们看到该方法其实就是对waiting_to_block计数进行操作,在signal_thread_at_safepoint方法中仅仅只是将waiting_to_block--。详细实现如下。

void ThreadSafepointState::roll_forward(suspend_type type) {

 _type = type;

 switch(_type) {

  // 首先调用signal_thread_at_safepoint减少_waiting_to_block计数,然后看看JNIcritical增加计数

  case _at_safepoint:

   SafepointSynchronize::signal_thread_at_safepoint();

   if (_thread->in_critical()) {

    SafepointSynchronize::increment_jni_active_count();

   }

   break;

  // 直接设置called_back为false,这里用于debug测试,不需要了解

  case _call_back:

   set_has_called_back(false);

   break;

  case _running:

  default:

   ShouldNotReachHere();

 }

}



static void signal_thread_at_safepoint()   { _waiting_to_block--; }
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

make_polling_page_unreadable方法原理

make_polling_page_unreadable方法用于将处于JIT编译优化后代码轮询检测线程安全点的polling_page进行操作。我们看到该方法其实就是调用Linux的系统调用mprotect,将数据页_polling_page设置为PROT_NONE,此时表示该页不允许被访问,那么当我们的线程访问该页时将会触发一个SIGSEGV信号,此时将会阻塞当前线程。详细原理如下。

void os::make_polling_page_unreadable(void) {

 if( !guard_memory((char*)_polling_page, Linux::page_size()) )

  fatal("Could not disable polling page");

};



bool os::guard_memory(char* addr, size_t size) {

 return linux_mprotect(addr, size, PROT_NONE);

}



static bool linux_mprotect(char* addr, size_t size, int prot) {

 char* bottom = (char*)align_size_down((intptr_t)addr, os::Linux::page_size());

 size = align_size_up(pointer_delta(addr, bottom, 1) + size, os::Linux::page_size());

 return ::mprotect(bottom, size, prot) == 0;

}
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

那么问题来了?有没有代码可以验证我们处理了这个SIGSEGV信号呢?我们继续看以下代码。当我们在信号处理函数中处理SIGSEGV信号时,将会对发生该信号的地址进行判断,如果判断为_polling_page页中的地址,那么将会调用get_poll_stub获取到检测安全点的代码执行。详细实现如下。

if (sig == SIGSEGV && os::is_poll_address((address)info->si_addr)) {

 stub = SharedRuntime::get_poll_stub(pc);

} 
1
2
3
4
5

os::make_polling_page_readable方法原理

该方法为make_polling_page_unreadable方法的逆向操作,将在解除线程安全点时调用。我们看到就是调用mprotect系统调用将页面修改为PROT_READ可读状态即可。详细原理如下。

void os::make_polling_page_readable(void) {

 if( !linux_mprotect((char *)_polling_page, Linux::page_size(), PROT_READ)) {

  fatal("Could not enable polling page");

 }

};
1
2
3
4
5
6
7
8
9

SafepointSynchronize::end方法原理

该方法用与解除线程安全点,恢复所有线程执行。

void SafepointSynchronize::end() { // 修改poling page为可访问状态 if (PageArmed) { os::make_polling_page_readable(); PageArmed = 0 ; } // 通知解释器移除对线程安全点的检测 Interpreter::ignore_safepoints(); { MutexLocker mu(Safepoint_lock); _state = _not_synchronized; // 修改线程安全点状态为未同步状态表明退出线程安全点 OrderAccess::fence(); // 使用全屏障使该修改对所有线程可见 // 遍历所有线程,将所有线程的线程安全点状态修改为_running状态 for(JavaThread current = Threads::first(); current; current = current->next()) { ThreadSafepointState cur_state = current->safepoint_state(); cur_state->restart(); // 修改线程状态为_running } Threads_lock->unlock(); // 修改完状态后释放线程锁,此时所有线程将会被唤醒 } #if INCLUDE_ALL_GCS // 通知GC线程已经退出线程安全点 if (UseConcMarkSweepGC) { ConcurrentMarkSweepThread::desynchronize(false); } else if (UseG1GC) { ConcurrentGCThread::safepoint_desynchronize(); } #endif // INCLUDE_ALL_GCS }

总结

我们通过原理描述得到以下信息:

  1. SafepointSynchronize::begin方法用于让所有线程进入线程安全点
  2. SafepointSynchronize::end方法用于唤醒所有处于安全点的线程
  3. 我们通过Thread_Lock来让线程处于阻塞状态(VMThread在系统线程安全点时一直持有该锁,其他线程通过该锁来等待)
  4. JIT编译的代码由于需要高效执行,我们通过设置不可访问页通过信号来处理线程安全点的检测和阻塞
  5. 我们可以通过serialize_thread_states来替代fence全屏障完成对线程状态修改的可见性,我们看到该方法也即利用Linux的页权限修改刷新CPU Store Buffer和TLB的修改来保证数据可见性,同时为了避免调度延迟的原因导致CPU空转一直执行SIGSEGV处理函数,那么采用了SerializePageLock来优化