JVM SafePoint安全点分析

本文深入探讨了JVM中的Safepoint机制,详细解释了Safepoint的概念及其在GC中的作用,同时介绍了HotspotVM中Safepoint的具体实现细节,包括线程如何被挂起和恢复的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是safepoint

safepoint可以用在不同地方,比如GC、Deoptimization,在Hotspot VM中,GC safepoint比较常见,需要一个数据结构记录每个线程的调用栈、寄存器等一些重要的数据区域里什么地方包含了GC管理的指针。

从线程角度看,safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停所有活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束。

什么地方可以放safepoint

下面以Hotspot为例,简单的说明一下什么地方会放置safepoint

1、理论上,在解释器的每条字节码的边界都可以放一个safepoint,不过挂在safepoint的调试符号信息要占用内存空间,如果每条机器码后面都加safepoint的话,需要保存大量的运行时数据,所以要尽量少放置safepoint,在safepoint会生成polling代码询问VM是否要“进入safepoint”,polling操作也是有开销的,polling操作会在后续解释。

2、通过JIT编译的代码里,会在所有方法的返回之前,以及所有非counted loop的循环(无界循环)回跳之前放置一个safepoint,为了防止发生GC需要STW时,该线程一直不能暂停。另外,JIT编译器在生成机器码的同时会为每个safepoint生成一些“调试符号信息”,为GC生成的符号信息是OopMap,指出栈上和寄存器里哪里有GC管理的指针。

线程如何被挂起

如果触发GC动作,VM thread会在VMThread::loop()方法中调用SafepointSynchronize::begin()方法,最终使所有的线程都进入到safepoint。

// Roll all threads forward to a safepoint and suspend them all
void SafepointSynchronize::begin() {
  Thread* myThread = Thread::current();
  assert(myThread->is_VM_thread(), "Only VM thread may execute a safepoint");

  if (PrintSafepointStatistics || PrintSafepointStatisticsTimeout > 0) {
    _safepoint_begin_time = os::javaTimeNanos();
    _ts_of_current_safepoint = tty->time_stamp().seconds();
  }

在safepoint实现中,有这样一段注释,Java threads可以有多种不同的状态,所以挂起的机制也不同,一共列举了5种情况:

这里写图片描述

1. 执行Java Code

在执行字节码的时候会检查safepoint的状态,因为在begin方法中会调用Interpreter::notice_safepoints()方法,通知解释器更新dispatch table,实现如下:

void TemplateInterpreter::notice_safepoints() {
  if (!_notice_safepoints) {
    // switch to safepoint dispatch table
    _notice_safepoints = true;
    copy_table((address*)&_safept_table, (address*)&_active_table, sizeof(_active_table) / sizeof(address));
  }
}

2. 执行native code

如果VM thread发现一个Java thread正在执行native code,并不会等待该Java thread阻塞,不过当该Java thread从native code返回时,必须检查safepoint状态,看是否需要进行阻塞。

这里涉及到两个状态:Java thread state和safepoint state,两者之间有着严格的读写顺序,一般可以通过内存屏障实现,但是性能开销比较大,Hotspot采用另一种方式,调用os::serialize_thread_states()把每个线程的状态依次写入到同一个内存页中,实现如下:

// Serialize all thread state variables
void os::serialize_thread_states() {
  // On some platforms such as Solaris & Linux, the time duration of the page
  // permission restoration is observed to be much longer than expected  due to
  // scheduler starvation problem etc. To avoid the long synchronization
  // time and expensive page trap spinning, 'SerializePageLock' is used to block
  // the mutator thread if such case is encountered. See bug 6546278 for details.
  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);
}

通过VM thread执行一系列mprotect os call,保证之前所有线程状态的写入可以被顺序执行,效率更高。

3. 执行complied code

如果想进入safepoint,则设置polling page不可读,当Java thread发现该内存页不可读时,最终会被阻塞挂起。在SafepointSynchronize::begin()方法中,通过os::make_polling_page_unreadable()方法设置polling page为不可读。

if (UseCompilerSafepoints && DeferPollingPageLoopCount < 0) {
    // Make polling safepoint aware
    guarantee (PageArmed == 0, "invariant") ;
    PageArmed = 1 ;
    os::make_polling_page_unreadable();
}

方法make_polling_page_unreadable()在不同系统的实现不一样

linux下实现
// Mark the polling page as unreadable
void os::make_polling_page_unreadable(void) {
  if( !guard_memory((char*)_polling_page, Linux::page_size()) )
    fatal("Could not disable polling page");
};

solaris下实现
// Mark the polling page as unreadable
void os::make_polling_page_unreadable(void) {
  if( mprotect((char *)_polling_page, page_size, PROT_NONE) != 0 )
    fatal("Could not disable polling page");
};

在JIT编译中,编译器会把safepoint检查的操作插入到机器码指令中,比如下面的指令:

0x01b6d627: call   0x01b2b210         ; OopMap{[60]=Oop off=460}      
                                       ;*invokeinterface size      
                                       ; - Client1::main@113 (line 23)      
                                       ;   {virtual_call}      
 0x01b6d62c: nop                       ; OopMap{[60]=Oop off=461}      
                                       ;*if_icmplt      
                                       ; - Client1::main@118 (line 23)      
 0x01b6d62d: test   %eax,0x160100      ;   {poll}      
 0x01b6d633: mov    0x50(%esp),%esi      
 0x01b6d637: cmp    %eax,%esi

test %eax,0x160100 就是一个检查polling page是否可读的操作,如果不可读,则该线程会被挂起等待。

4. 线程处于Block状态

即使线程已经满足了block condition,也要等到safepoint operation完成,如GC操作,才能返回。

5. 线程正在转换状态

会去检查safepoint状态,如果需要阻塞,就把自己挂起。

最终实现

当线程访问到被保护的内存地址时,会触发一个SIGSEGV信号,进而触发JVM的signal handler来阻塞这个线程,The GC thread can protect some memory to which all threads in the process can write (using the mprotect system call) so they no longer can. Upon accessing this temporarily forbidden memory, a signal handler kicks in。

再看看底层是如何处理这个SIGSEGV信号,实现位于hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp

// Check to see if we caught the safepoint code in the
// process of write protecting the memory serialization page.
// It write enables the page immediately after protecting it
// so we can just return to retry the write.
if ((sig == SIGSEGV) &&
    os::is_memory_serialize_page(thread, (address) info->si_addr)) {
  // Block current thread until the memory serialize page permission restored.
  os::block_on_serialize_page_trap();
  return true;
}

执行os::block_on_serialize_page_trap()把当前线程阻塞挂起。

线程如何恢复

有了begin方法,自然有对应的end方法,在SafepointSynchronize::end()中,会最终唤醒所有挂起等待的线程,大概实现如下:
1、重新设置pooling page为可读

 if (PageArmed) {
    // Make polling safepoint aware
    os::make_polling_page_readable();
    PageArmed = 0 ;
  }

2、设置解释器为ignore_safepoints,实现如下:

// switch from the dispatch table which notices safepoints back to the
// normal dispatch table.  So that we can notice single stepping points,
// keep the safepoint dispatch table if we are single stepping in JVMTI.
// Note that the should_post_single_step test is exactly as fast as the
// JvmtiExport::_enabled test and covers both cases.
void TemplateInterpreter::ignore_safepoints() {
  if (_notice_safepoints) {
    if (!JvmtiExport::should_post_single_step()) {
      // switch to normal dispatch table
      _notice_safepoints = false;
      copy_table((address*)&_normal_table, (address*)&_active_table, sizeof(_active_table) / sizeof(address));
    }
  }
}

3、唤醒所有挂起等待的线程

// Start suspended threads
    for(JavaThread *current = Threads::first(); current; current = current->next()) {
      // A problem occurring on Solaris is when attempting to restart threads
      // the first #cpus - 1 go well, but then the VMThread is preempted when we get
      // to the next one (since it has been running the longest).  We then have
      // to wait for a cpu to become available before we can continue restarting
      // threads.
      // FIXME: This causes the performance of the VM to degrade when active and with
      // large numbers of threads.  Apparently this is due to the synchronous nature
      // of suspending threads.
      //
      // TODO-FIXME: the comments above are vestigial and no longer apply.
      // Furthermore, using solaris' schedctl in this particular context confers no benefit
      if (VMThreadHintNoPreempt) {
        os::hint_no_preempt();
      }
      ThreadSafepointState* cur_state = current->safepoint_state();
      assert(cur_state->type() != ThreadSafepointState::_running, "Thread not suspended at safepoint");
      cur_state->restart();
      assert(cur_state->is_running(), "safepoint state has not been reset");
    }

对JVM性能有什么影响

通过设置JVM参数 -XX:+PrintGCApplicationStoppedTime, 可以打出系统停止的时间,大概如下:

Total time for which application threads were stopped: 0.0051000 seconds  
Total time for which application threads were stopped: 0.0041930 seconds  
Total time for which application threads were stopped: 0.0051210 seconds  
Total time for which application threads were stopped: 0.0050940 seconds  
Total time for which application threads were stopped: 0.0058720 seconds  
Total time for which application threads were stopped: 5.1298200 seconds
Total time for which application threads were stopped: 0.0197290 seconds  
Total time for which application threads were stopped: 0.0087590 seconds

从上面数据可以发现,有一次暂停时间特别长,达到了5秒多,这在线上环境肯定是无法忍受的,那么是什么原因导致的呢?

一个大概率的原因是当发生GC时,有线程迟迟进入不到safepoint进行阻塞,导致其他已经停止的线程也一直等待,VM Thread也在等待所有的Java线程挂起才能开始GC,这里需要分析业务代码中是否存在有界的大循环逻辑,可能在JIT优化时,这些循环操作没有插入safepoint检查。

來源:简书 http://www.jianshu.com/p/c79c5e02ebe6

<think>嗯,用户问的是JVM对象的安全点是什么。我需要先回忆一下安全点Safepoint)的相关知识,确保自己理解正确。首先,安全点JVM中的一个重要概念,主要用于在垃圾回收(GC)和代码优化等操作时,让所有线程到达一个一致的状态。这时候线程会被暂停,以便JVM执行需要所有线程协作的任务。 接下来,我要考虑用户可能的背景。用户可能是Java开发者,或者对JVM内部机制感兴趣的人。他们可能遇到了性能问题,或者想深入了解GC的工作原理。用户的问题比较基础,但需要详细且清晰的解释,所以需要分步骤说明,结构要清晰。 首先,定义安全点安全点是程序执行中的特定位置,线程执行到这些位置时,会检查是否需要进入安全点。这里需要强调,安全点的存在是为了让JVM安全地暂停线程,进行全局操作,比如GC。 然后,要解释为什么需要安全点。例如,在GC时,JVM需要确保所有线程的内存状态是一致的,这样才能安全地回收不再使用的对象。如果线程在GC时还在修改对象引用,可能会导致数据不一致或内存错误。 接下来,需要说明安全点如何工作。当JVM需要所有线程到达安全点时,会设置一个标志。线程在执行过程中,会在特定的检查点(比如方法返回、循环跳转、异常抛出等位置)检查这个标志。如果检测到需要进入安全点线程会主动暂停,直到JVM完成操作。 然后,可能需要提到安全点的实现机制。例如,JIT编译器在生成代码时,会在这些检查点插入检查指令。不同的JVM可能有不同的策略,比如使用轮询或主动挂起线程。 还要考虑安全点对性能的影响。因为线程需要频繁检查安全点标志,可能会带来一定的开销。特别是在高并发或大量线程的情况下,安全点的停顿时间可能影响应用的响应速度。因此,JVM会优化安全点的位置和检查频率,减少性能损耗。 此外,用户可能想知道哪些操作会触发安全点。除了GC,还有偏向锁撤销、代码反优化(如分层编译中的降级)、线程栈采样等。这些情况下都需要线程进入安全点,确保状态一致。 最后,可能需要给出一些实际的应用场景或优化建议。例如,避免在热点代码中长时间运行的循环,因为这样的循环可能没有足够的安全点检查,导致线程无法及时响应安全点请求,从而增加停顿时间。 需要确保使用中文,结构清晰,分点说明,并且遵循系统级指令中的格式要求。数学表达式虽然在这个问题中可能用不上,但如果有的话要正确使用。同时,避免使用Markdown,保持口语化但专业。</think>好的,我来逐步解释JVM中的“安全点”(Safepoint)概念: --- ### 1. **什么是安全点?** 安全点是**程序执行过程中的特定位置**,当JVM需要执行某些需要“全局暂停”的操作(如垃圾回收、代码反优化)时,必须确保所有线程都到达这些位置并暂停。 此时线程的状态是已知且一致的,JVM可以安全地进行操作,而不会破坏程序逻辑。 --- ### 2. **为什么需要安全点?** - **关键场景**: - 垃圾回收(GC):需冻结所有线程,防止对象引用被修改。 - 代码反优化:例如JIT编译后的代码需要回退到解释模式。 - 偏向锁撤销:解决多线程竞争时的锁状态问题。 - **核心目标**:确保所有线程处于可控状态,避免因线程异步操作导致内存不一致或逻辑错误。 --- ### 3. **安全点如何工作?** - **触发条件**:当JVM需要执行全局操作时(如GC),会设置一个全局标志。 - **线程响应**: 每个线程运行到**安全点检查位置**时(例如:方法返回、循环末尾、异常抛出处),会检查该标志。 若检测到需要暂停,线程会主动挂起,直到JVM完成操作。 --- ### 4. **安全点的实现机制** - **插入检查点**:JIT编译器在编译代码时,会在特定位置插入安全点检查(如循环回边或方法出口)。 - **两种策略**: - **主动式**:线程定期轮询安全点标志(通过内存读操作)。 - **协作式**:依赖线程主动挂起(现代JVM常用)。 --- ### 5. **安全点对性能的影响** - **停顿时间(Stop-The-World)**:所有线程到达安全点的时间总和会影响GC暂停时间。 - **优化方向**: - 减少长时间循环中缺少安全点的情况(例如:循环内增加空操作)。 - 避免在关键代码路径中执行耗时操作。 --- ### 6. **示例场景** 假设一个线程正在执行以下循环: ```java while (true) { // 没有方法调用或循环边界(早期JVM可能无法插入安全点) } ``` 如果循环中没有安全点检查,线程可能长时间无法响应GC请求,导致JVM停顿时间变长。 --- ### 总结 安全点JVM协调多线程的关键机制,通过全局暂停保证内存一致性和操作安全性。理解它对优化GC性能、减少应用停顿时间有重要意义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值