JVM

JVM-14丨SafePoint(安全点)

Posted by jiefang on December 13, 2019

SafePoint

简介

A point in program where the state of execution is known by the VM,即代码中VM能够准确知道执行状态的位置。

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

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

安全点

根节点枚举

HotSpot虚拟机使用可达性分析算法确定对象是否可以被GC。
可达性分析算法从一系列GCRoot对象开始,向下搜索引用链,如果一个对象没有与任何GCRoot对象关联,这个对象就会被判定为可回收对象。过程称之为根节点枚举,也就是垃圾回收中的标记过程。

GCRoot包括以下对象:

  • 虚拟机栈上的本地变量表引用的对象;
  • 方法区中类的静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI引用的对象;

SafePoint是程序中的某些位置,线程执行到这些位置时,线程中的某些状态是确定的,在safePoint可以记录OopMap信息,线程在safePoint停顿,虚拟机进行GC。一个线程可以在SafePoint上,也可以不在SafePoint上。一个线程在SafePoint时,它的状态可以安全地被其他JVM线程所操作和观测。

线程停顿方式有两种,抢先式中断和主动式中断:

  • 抢先式中断:虚拟机需要GC时,中断所有线程,让没有到达SafePoint的线程继续执行至SafePoint并中断;
  • 主动式中断:虚拟机不直接中断线程,而是在内存中设置标志位,线程检查到标志位被设置,运行至SafePoint时主动中断;

hotspot采用的是主动检测的方式. 而主动检测的方式中又分为两种方式:

  • 指定点执行检测代码
  • polling page访问异常触发

    hotspot会根据运行时的信息来统计, 并将高频率执行的java字节码直接翻译成本地代码, 由此提高执行效率. 因此, hotspot有两种执行方式, 一个是解释执行, 一个是编译执行。指定点检测主要是解释执行用的,对于需要高效实现的地方,则采用polling page。
    polling page和普通物理页面没什么区别,需要safepoint时, 会修改该页面的权限为不可访问, 这样编译的代码在访问这个页面时, 会触发段违规异常(SEGEV). 而hotspot在启动时捕获了这个异常, 当意识到是访问polling page导致时, 则主动挂起。

SafePoint一般出现在以下位置:

  • 循环体的结尾;
  • 方法返回前;
  • 调用方法的call之后;
  • 抛出异常的位置;

线程挂起

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void VMThread::loop() {
    if (timedout && (SafepointALot ||
                     SafepointSynchronize::is_cleanup_needed())) {
      MutexUnlockerEx mul(VMOperationQueue_lock,
                          Mutex::_no_safepoint_check_flag);
      // Force a safepoint since we have not had one for at least
      // 'GuaranteedSafepointInterval' milliseconds.  This will run all
      // the clean-up processing that needs to be done regularly at a
      // safepoint
      SafepointSynchronize::begin();
      #ifdef ASSERT
        if (GCALotAtAllSafepoints) InterfaceSupport::check_gc_alot();
      #endif
      SafepointSynchronize::end();
    }
}

### 线程有五种不同的状态对应五种挂起的措施

  • 执行Java code

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

  • 执行native code

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

  • 执行complied code

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

  • 线程处于Block状态

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

  • 线程正在转换状态

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

线程恢复

有了begin方法,自然有对应的end方法,在SafepointSynchronize::end()中,会最终唤醒所有挂起等待的线程,大概实现如下:

  1. 重新设置pooling page为可读;
  2. 设置解释器为ignore_safepoints;
  3. 唤醒所有挂起等待的线程;

对JVM的影像

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

image

安全区域(SafeRegion)

安全点机制保证了程序执行的时候,在不太长的时间就会遇到可进入gc的安全点。但是如果线程处于sleep状态或者blocked状态的时候,这时线程无法响应jvm的中断请求,就需要安全区域

安全区域是指在一段代码片段中,引用关系不会发生变化,在该区域的任何地方发生gc都是安全的。

  • 线程进入SafeRegion会给自己加标记,告诉虚拟机可以进行GC;
  • 线程准备离开SafeRegion前会询问虚拟机GC是否完成。