JVM系列之:再谈java中的safepoint说明

safepoint是什么

java程序里面有很多很多的java线程,每个java线程又有自己的stack,并且共享了heap。这些线程一直运行呀运行,不断对stack和heap进行操作。

这个时候如果JVM需要对stack和heap做一些操作该怎么办呢?

比如JVM要进行GC操作,或者要做heap dump等等,这时候如果线程都在对stack或者heap进行修改,那么将不是一个稳定的状态。GC直接在这种情况下操作stack或者heap,会导致线程的异常。

怎么处理呢?

这个时候safepoint就出场了。

safepoint就是一个安全点,所有的线程执行到安全点的时候就会去检查是否需要执行safepoint操作,如果需要执行,那么所有的线程都将会等待,直到所有的线程进入safepoint。

然后JVM执行相应的操作之后,所有的线程再恢复执行。

safepoint的例子

我们举个例子,一般safepoint比如容易出现在循环遍历的情况,还是使用我们之前做null测试用的例子:

public class TestNull {

 public static void main(String[] args) throws InterruptedException {
  List<String> list= new ArrayList();
 list.add("www.flydean.com");
  for (int i = 0; i < 10000; i++)
  {
   testMethod(list);
  }
  Thread.sleep(1000);
 }

 private static void testMethod(List<String> list)
 {
  list.get(0);
 }
}

运行结果如下:

标红的就是传说中的safepoint。

线程什么时候会进入safepoint

那么线程什么时候会进入safepoint呢?

一般来说,如果线程在竞争锁被阻塞,IO被阻塞,或者在等待获得监视器锁状态时,线程就处于safepoint状态。

如果线程再执行JNI代码的哪一个时刻,java线程也处于safepoint状态。因为java线程在执行本地代码之前,需要保存堆栈的状态,让后再移交给native方法。

如果java的字节码正在执行,那么我们不能判断该线程是不是在safepint上。

safepoint是怎么工作的

如果你使用的是hotspot JVM,那么这个safepoint是一个全局的safepoint,也就是说执行Safepoint需要暂停所有的线程。

如果你使用的是Zing,那么可以在线程级别使用safepoint。

我们可以看到生成的汇编语言中safepoint其实是一个test命令。

test指向的是一个特殊的内存页面地址,当JVM需要所有的线程都执行到safepint的时候,就会对该页面做一个标记。从而通知所有的线程。

我们再用一张图来详细说明:

thread1在收到设置safepoint之前是一直执行的,在收到信号之后还会执行一段时间,然后到达Safepint暂停执行。

thread2先执行了一段时间,然后因为CPU被抢夺,空闲了一段时间,在这段时间里面,thread2收到了设置safepoint的信号,然后thread2获得执行权力,接着继续执行,最后到达safepoint。

thread3是一个native方法,将会一直执行,知道safepoint结束。

thread4也是一个native方法,它和thread3的区别就在于,thread4在safepoint开始和结束之间结束了,需要将控制器转交给普通的java线程,因为这个时候JVM在执行Safepoint的操作,所以任然需要暂停执行。

在HotSpot VM中,你可以在汇编语言中看到safepoint的两种形式:'{poll}' 或者 ‘{poll return}' 。

总结

本文详细的讲解了JVM中Safepoint的作用,希望大家能够喜欢。

补充知识:JVM源码分析之安全点safepoint

上周有幸参加了一次关于JVM的小范围分享会,听完R大对虚拟机C2编译器的讲解,我的膝盖一直是肿的,能记住的实在有点少,能听进去也不多

1、什么时候进行C2编译,如何进行C2编译(这个实在太复杂)

2、C2编译的时候,是对整个方法体进行编译,而不是某个方法段

3、JVM中的safepoint

一直都知道,当发生GC时,正在执行Java code的线程必须全部停下来,才可以进行垃圾回收,这就是熟悉的STW(stop the world),但是STW的背后实现原理,比如这些线程如何暂停、又如何恢复?就比较疑惑了。

然而这一切的一切,都涉及到一个概念safepoint,openjdk的实现位于

openjdk/hotspot/src/share/vm/runtime/safepoint.cpp

什么是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检查。

以上这篇JVM系列之:再谈java中的safepoint说明就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • java安全编码指南之:对象构建操作

    简介 程序员肯定是不缺对象的,因为随时都可以构建一个,对象多了肯定会出现点安全问题,一起来看看在java的对象构建中怎么保证对象的安全性吧. 构造函数的异常 考虑下面的一个例子: public class SensitiveOperation { public SensitiveOperation(){ if(!doSecurityCheck()){ throw new SecurityException("Security check failed!"); } } //Securit

  • java安全编码指南之:表达式规则说明

    简介 在java编写过程中,我们会使用到各种各样的表达式,在使用表达式的过程中,有哪些安全问题需要我们注意的呢?一起来看看吧. 注意表达式的返回值 我们在使用JDK库的时候,一定要注意认真的读一下JDK中方法的含义和它的返回值. 有些返回值可能表示这个操作是否成功,有的返回值可能是方法操作的结果.我们看两个常见的例子: public void deleteFileWrong(){ File file= new File("/tmp/www.jb51.net.txt"); file.de

  • java安全编码指南之:Number操作详解

    简介 java中可以被称为Number的有byte,short,int,long,float,double和char,我们在使用这些Nubmer的过程中,需要注意些什么内容呢?一起来看看吧. Number的范围 每种Number类型都有它的范围,我们看下java中Number类型的范围: 考虑到我们最常用的int操作,虽然int的范围够大,但是如果我们在做一些int操作的时候还是可能超出int的范围. 超出了int范围会发送什么事情呢?看下面的例子: public void testIntege

  • java安全编码指南之:Mutability可变性详解

    简介 mutable(可变)和immutable(不可变)对象是我们在java程序编写的过程中经常会使用到的. 可变类型对象就是说,对象在创建之后,其内部的数据可能会被修改.所以它的安全性没有保证. 而不可变类型对象就是说,对象一旦创建之后,其内部的数据就不能够被修改,我们可以完全相信这个对象. 虽然mutable对象安全性不够,但是因为其可以被修改,所以会有效的减少对该对象的拷贝. 而immutable对象因为不可改变,所以尝试对该对象的修改都会导致对象的拷贝,从而生成新的对象. 我们最常使用

  • java安全编码指南之:声明和初始化说明

    简介 在java对象和字段的初始化过程中会遇到哪些安全性问题呢?一起来看看吧. 初始化顺序 根据JLS(Java Language Specification)中的定义,class在初始化过程中,需要同时初始化class中定义的静态初始化程序和在该类中声明的静态字段(类变量)的初始化程序. 而对于static变量来说,如果static变量被定义为final并且它值是编译时常量值,那么该static变量将会被优先初始化. 那么使用了final static变量,是不是就没有初始化问题了呢? 我们来

  • 在java中使用SPI创建可扩展的应用程序操作

    简介 什么是可扩展的应用程序呢?可扩展的意思是不需要修改原始代码,就可以扩展应用程序的功能.我们将应用程序做成插件或者模块. 这样可以在不修改原应用的基础上,对系统功能进行升级或者定制化. 本文将会向大家介绍如何通过java中的SPI机制实现这种可扩展的应用程序. SPI简介 SPI的全称是Java Service Provider Interface.是java提供的一种服务发现的机制. 通过遵循相应的规则编写应用程序之后,就可以使用ServiceLoader来加载相应的服务了. SPI的实现

  • JVM系列之:再谈java中的safepoint说明

    safepoint是什么 java程序里面有很多很多的java线程,每个java线程又有自己的stack,并且共享了heap.这些线程一直运行呀运行,不断对stack和heap进行操作. 这个时候如果JVM需要对stack和heap做一些操作该怎么办呢? 比如JVM要进行GC操作,或者要做heap dump等等,这时候如果线程都在对stack或者heap进行修改,那么将不是一个稳定的状态.GC直接在这种情况下操作stack或者heap,会导致线程的异常. 怎么处理呢? 这个时候safepoint

  • 浅谈Java中的atomic包实现原理及应用

    1.同步问题的提出 假设我们使用一个双核处理器执行A和B两个线程,核1执行A线程,而核2执行B线程,这两个线程现在都要对名为obj的对象的成员变量i进行加1操作,假设i的初始值为0,理论上两个线程运行后i的值应该变成2,但实际上很有可能结果为1. 我们现在来分析原因,这里为了分析的简单,我们不考虑缓存的情况,实际上有缓存会使结果为1的可能性增大.A线程将内存中的变量i读取到核1算数运算单元中,然后进行加1操作,再将这个计算结果写回到内存中,因为上述操作不是原子操作,只要B线程在A线程将i增加1的

  • 浅谈Java中是否直接可以使用enum进行传输

    背景 我们在进行传输的时候 会有一些状态值,如Status为1代表删除,为0代表失败或者怎么样的.只传输一个)0或者1过去给第三方(此处不包括给前端),如果没有契约第三方会不认识你这个是什么意思,那我们在平时写业务逻辑的时候使用枚举很轻易就知道了什么状态什么值.所以我们在构建DTO对象的时候里面放一个枚举来表示. 首先在阿里的规范里是这样说的: [强制]二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象. 那到底为啥不能用呢? 枚举

  • 浅谈Java中File文件的创建以及读写

    1.创建一个文件 @Test public void test6() throws IOException { File file1 = new File("C:\\IDEA\\h1.txt"); if(!file1.exists()){//文件不存在 file1.createNewFile(); System.out.println("创建成功"); }else{//文件存在 file1.delete(); System.out.println("删除成

  • 浅谈Java中Unicode的编码和实现

    Unicode的编码和实现 大概来说,Unicode编码系统可分为编码方式和实现方式两个层次. 编码方式 字符是抽象的最小文本单位.它没有固定的形状(可能是一个字形),而且没有值."A"是一个字符,"€"也是一个字符.字符集是字符的集合.编码字符集是一个字符集,它为每一个字符分配一个唯一数字. Unicode 最初设计是作为一种固定宽度的 16 位字符编码.也就是每个字符占用2个字节.这样理论上一共最多可以表示216(即65536)个字符.上述16位统一码字符构成基

  • 浅谈Java中的四种引用方式的区别

    强引用.软引用.弱引用.虚引用的概念 强引用(StrongReference) 强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的object和str都是强引用: Object object = new Object(); String str = "hello"; 只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象. 比如下面这段代码: public class Main { publi

  • 浅谈java中为什么实体类需要实现序列化

    当客户端访问某个能开启会话功能的资源,web服务器就会创建一个HTTPSession对象,每个HTTPSession对象都会占用一定的内存,如果在同一个时间段内访问的用户太多,就会消耗大量的服务器内存,为了解决这个问题我们使用一种技术:session的持久化. 什么是session的持久化? web服务器会把暂时不活动的并且没有失效的HTTPSession对象转移到文件系统或数据库中储存,服务器要用时在把他们转载到内存. 把Session对象转移到文件系统或数据库中储存就需要用到序列化: jav

  • 浅谈java中BigDecimal的equals与compareTo的区别

    这两天在处理支付金额校验的时候出现了点问题,有个金额比较我用了BigDecimal的equals方法来比较两个金额是否相等,结果导致金额比较出现错误(比如3.0与3.00的比较等). [注:以下所讲都是以sun jdk 1.4.2版本为例,其他版本实现未必一致,请忽略] 首先看一下BigDecimal的equals方法: public boolean equals(Object x){ if (!(x instanceof BigDecimal)) return false; BigDecima

  • 浅谈Java中方法参数传递的问题

    可以理解当我们要调用一个方法时,我们会把指定的数值,传递给方法中的参数,这样方法中的参数就拥有了这个指定的值,可以使用该值,在方法中运算了.这种传递方式,我们称为参数传递.在这里,定义方法时,参数列表中的变量,我们称为形式参数. 调用方法时,传入给方法的数值,我们称为实际参数 在Java中调用方法时,如果参数是基本类型(byte/short/int/long/float/double/char/boolean)以及String类型时,形式参数的改变不影响实际参数. 以下代码在内存中发生的动作:

  • 浅谈java中Math.random()与java.util.random()的区别

    今天突然想起来,java产生随机数的问题,上机试了一下,找到了一点区别,在这里总结一下: 直接调用Math.random()是产生一个[0,1)之间的随机数, 如果用 java.util.Random random=new Random();random.nextInt() 这样产生一个长整型的随机数并且与上一次是一样的,如果过一会再产生就不会一样了,例如: for (int i = 0; i < 10; i++) { Random random=new Random(); Thread.sle

随机推荐