JVM GC 的安全点与安全区域

JVM GC 的安全点与安全区域

OopMap

JVM 采用的可达性分析法有个缺点,就是从 GC Roots 找引用链耗时。

都说他耗时,他究竟耗时在哪里?
GC 进行扫描时,需要查看每个位置存储的是不是引用类型,如果是,其所引用的对象就不能被回收;如果不是,那就是基本类型,这些肯定是不会引用对象的;这种对 GC 无用的基本类型的数据非常多,每次 GC 都要去扫描,显然是非常浪费时间的。
而且迄今为止,所有收集器在 GC Roots 枚举这一步骤都是必须暂停用户线程的。

那有没有办法减少耗时呢?
一个很自然的想法,能不能用空间换时间? 把栈上的引用类型的位置全部记录下来,这样到 GC 的时候就可以直接读取,而不用一个个扫描了。Hotspot 就是这么实现的,这个用于存储引用类型的数据结构叫 OopMap
OopMap 这个词可以拆成两部分:OopMapOop 的全称是 Ordinary Object Pointer 普通对象指针,Map 大家都知道是映射表,组合起来就是 普通对象指针映射表。

OopMap 的协助下,HotSpot 就能快速准确地完成 GC Roots 枚举啦。

安全点

OopMap 的更新,从直观上来说,需要在对象引用关系发生变化的时候修改。不过导致引用关系变化的指令非常多,如果对每条指令都记录 OopMap 的话 ,那将会需要大量的额外存储空间,空间成本就会变得无法忍受的高昂。选用一些特定的点来记录就能有效的缩小需要记录的数据量,这些特定的点就称为 安全点 (Safepoint)

有了安全点,当 GC 回收需要停止用户线程的时候,将设置某个中断标志位,各个线程不断轮询这个标志位,发现需要挂起时,自己跑到最近的安全点,更新完 OopMap 才能挂起。这主动式中断的方式是绝大部分现代虚拟机选择的方案,另一种抢占式就不介绍了。

安全点不是任意的选择,既不能太少以至于让收集器等待时间过长,也不能过多以至于过分增大运行时的内存负荷。通常选择一些执行时间较长的指令作为安全点,如方法调用循环跳转异常跳转等。

安全区域

使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了。但是,如果此时线程正处于 Sleep 或者 Blocked 状态,该怎么办?这些线程他不会自己走到安全点,就停不下来了。这个时候,安全点解决不了问题,需要引入 安全区域 (Safe Region)

安全区域指的是,在某段代码中,引用关系不会发生变化,线程执行到这个区域是可以安全停下进行 GC 的。因此,我们也可以把 安全区域 看做是扩展的安全点。

当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域。那样当这段时间里虚拟机要发起 GC 时,就不必去管这些在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否处于 STW 状态,如果是,则需要等待直到恢复。


Native Monitors Design

https://wiki.openjdk.org/display/HotSpot/Native+Monitors+Design

当前的本地监控器设计使用了一种我们称之为 "偷偷加锁 "的技术,以防止 JVM 在安全点期间出现死锁。不过,当监控器在 VMThread 和非 JavaThreads 之间共享时,这种技术的实现会引入竞争。此外,当前的设计使用了大量优化技术来提高性能,从而使代码变得更加复杂。也许我们可以在不影响性能的情况下简化代码。因此,本提案的目标就是通过重新设计本地监控器的实现方式来修复这种偷偷摸摸的方案,同时简化代码并保持与当前相当的性能。

回顾: 偷偷摸摸的锁定
如果在安全点期间,VMThread 试图获取外部锁(实际锁字)已被 JavaThread 获取的监视器,而 JavaThread 正在阻塞等待安全点结束,那么偷偷摸摸的锁定就能防止死锁情况的发生。下面是方法 Monitor::lock() 的骨架:

void Monitor::lock(Thread * Self) {
 if (TryLock()) {
   set_owner(Self);
   return;
 }
 if (Self->is_Java_thread()) {
   ThreadBlockInVM tbivm((JavaThread *) Self);
   ILock(Self);
 } else {
   ILock(Self);
 }
 set_owner(Self);
}
下面是在 Monitor:wait() 中对 ThreadBlockInVM 的类似使用
bool Monitor::wait() {
 set_owner(NULL);
 if (no_safepoint_check) {
   wait_status = IWait(Self, timeout);
 } else {
   ThreadBlockInVM tbivm(jt);
   wait_status = IWait(Self, timeout);
 }
 set_owner(Self);
}

如果我们想让安全点继续运行,就需要在 ThreadBlockInVM 构造函数中执行从 _thread_in_vm_thread_blocked 的状态转换,因为 ILock() 调用可能会阻塞操作系统的调用。在 ThreadBlockInVM 对象的构造函数和析构函数中还执行了 SafepointMechanism::block_if_requested() 调用。为什么需要在析构函数中执行?因为在 ILock() 调用返回后,如果当前存在安全点,JavaThread 就必须停止,不能继续执行虚拟机代码。
由于希望在获取本地监控器时检查安全点,如果监控器在 JavaThreads VMThread 之间共享,我们现在可能会遇到死锁问题。因此,我们在 Monitor::lock 中添加了以下代码,以实现 "偷偷加锁":

bool can_sneak = Self->is_VM_thread() && SafepointSynchronize::is_at_safepoint();
if (can_sneak && _owner == NULL) {
 _snuck = true;
 set_owner(Self);
}

正如我们之前提到的,如果非 Java 线程也试图获取监视器,这段代码就会引入竞争,因为非 Java 线程可能已经锁定监视器,但尚未将自己设置为所有者。(使用 Monitor::try_lock() 也会产生同样的竞争)。非 JavaThreads 指的是虚拟机内部线程,如 ConcurrentGCThreadsWorkerThreadsWatcherThreadJfrThreadSampler 等。
值得一提的是,我们可以保留当前的本地监控器设计,并修改潜行代码部分,以便在潜行前实际识别哪种线程拥有外锁(例如,我们可以使用锁字的第二位)。

设计建议–>平台监控器

该提案基于新类 PlatformMonitor 的引入,它是各平台中实际同步原语(mutexes 和 condition variables)的封装器,并为本地监控器的实现提供了基本 API。以下是 PlatformMonitor 类的定义(适用于 posix 操作系统;对于 solaris 和 windows,也有相应的定义):

// 支持虚拟机监控器/Mutex 类的平台特定实现

class PlatformMonitor : public CHeapObj<mtInternal> {
 private:
   pthread_mutex_t _mutex[1]; // 用于锁定的本地 mutex
   pthread_cond_t _cond[1]; // 用于阻塞的本地条件变量
 public:
   PlatformMonitor();
   void lock();
   void unlock();
   bool try_lock();
   int wait(jlong millis);
   void notify();
   void notify_all();
};

举例来说,我们可以看到PlatformMonitor::lock() pthread_mutex_lock() 的包装器,而PlatformMonitor::wait() 只是使用底层的pthread_cond_timedwait() 调用,首先解析等待时间:

void os::PlatformMonitor::lock() {
 int status = pthread_mutex_lock(_mutex);
 assert_status(status == 0, status, "mutex_lock");
}
// 必须已经锁定
int os::PlatformMonitor::wait(jlong millis) {
 assert(millis >= 0, "negative timeout");
 if (millis > 0) {
   struct timespec abst;
   // 在将 millis 转换为 nanos 时,我们必须注意溢出、
   // 但如果 millis 这么大,我们最终还是会限制到
   // MAX_SECS,所以就在这里做吧。
   if (millis / MILLIUNITS > MAX_SECS) {
     millis = jlong(MAX_SECS) * MILLIUNITS;
   }
   to_abstime(&abst, millis * (NANOUNITS / MILLIUNITS), false);
   int ret = OS_TIMEOUT;
   int status = pthread_cond_timedwait(_cond, _mutex, &abst);
   assert_status(status == 0 || status == ETIMEDOUT, status, "cond_timedwait");
   if (status == 0) {
     ret = OS_OK;
   }   
   return ret;
 } else {
   int status = pthread_cond_wait(_cond, _mutex);
   assert_status(status == 0, status, "cond_wait");
   return OS_OK;
 }
}

总体设计策略
如前所述,实现本地监控器的想法主要是依靠简单的底层PlatformMonitor 类。然而,为了能够删除偷锁代码,同时避免死锁情形,我们必须能够解决这两个问题情形:

  • Monitor::lock()Monitor::wait() 中,阻塞ThreadBlockInVM 析构函数中的安全点可能导致死锁,因为监控器可能与 VMThread 共享。

-在监控器::wait()中,阻塞ThreadBlockInVM 构造函数中的安全点可能会导致死锁,因为我们已经拥有了监控器,而且和之前一样,监控器可能与 VMThread 共享。在进入构造函数之前释放锁也不是办法,因为我们可能会错过notify() 调用。

为了处理第一种情况,我们将让刚刚获得锁但在 ThreadBlockInVM 析构函数中检测到有安全点请求的 JavaThread 在阻塞安全点之前释放锁。从安全点恢复后,Java 线程必须再次获取锁,因为它刚刚调用了 Monitor::lock()。对于第二种情况,我们将更改ThreadBlockInVM 构造函数,允许可能的安全点请求取得进展,但不让 JavaThread 为此阻塞。为了避免在当前的 ThreadBlockInVM 外壳中添加额外的条件来实现此功能,我们将创建并使用一个新的外壳 ThreadBlockInVMWithDeadlockCheck

监控器::lock()的实现
以下是Monitor::lock() 的实现(不含额外的检查和断言):

void Monitor::lock(Thread * self) {
 Monitor* in_flight_monitor = NULL;
 while (!_lock.try_lock()) {
   // The lock is contended
   if (self->is_Java_thread()) {
     ThreadBlockInVMWithDeadlockCheck tbivmdc((JavaThread *) self, &in_flight_monitor);
     in_flight_monitor = this;
     _lock.lock();
   }
   if (in_flight_monitor != NULL) {
     break;
   } else {
     _lock.lock();
    break;
   }
 }
 set_owner(self);
}

JavaThreadVMThread 的锁定路径非常简单。对于JavaThreads,每当我们从TBIVWDC 外壳返回时,如果没有停在安全点,我们就会将自己设置为所有者并返回。如果我们确实停在了安全点,那么in_flight_monitor 本地变量将为 NULL,因此我们将再次循环尝试获取监控器。将 in_flight_monitor 设为 NULL 并释放监视器的代码如下所示。该代码由TBIVWDC 析构函数调用,以防我们需要阻塞安全点或握手:

void release_monitor() {
 Monitor* in_flight_monitor = *_in_flight_monitor_adr;
 if (in_flight_monitor != NULL) {
   in_flight_monitor->release_for_safepoint();
   *_in_flight_monitor_adr = NULL;
 }
}

release_for_safepoint() 只是释放锁。

监控器::wait()的实现
下面是 Monitor::wait() 的实现(没有额外的检查和断言):

bool Monitor::wait(bool no_safepoint_check, long timeout, bool as_suspend_equivalent) {
 int wait_status;
 set_owner(NULL);
 if (no_safepoint_check) {
   wait_status = _lock.wait(timeout);
   set_owner(self);
 } else {
   JavaThread *jt = (JavaThread *)self;
   Monitor* in_flight_monitor = NULL;
   {
     ThreadBlockInVMWithDeadlockCheck tbivmdc(jt, &in_flight_monitor);
     OSThreadWaitState osts(self->osthread(), false /* not Object.wait() */);
     if (as_suspend_equivalent) {
       jt->set_suspend_equivalent();
     }
     wait_status = _lock.wait(timeout);
     in_flight_monitor = this; 
     if (as_suspend_equivalent && jt->handle_special_suspend_equivalent_condition()) {
       _lock.unlock();
       jt->java_suspend_self();
       _lock.lock();
     }
   }
   if (in_flight_monitor != NULL) {
     set_owner(self);
   } else {
     lock(self);
   }
 }
 return wait_status != 0; // return true IFF timeout
}

in_flight_monitor 本地变量的使用类似于 lock() 的情况。在TBIVWDC 构造函数中,如果存在待处理的安全点,我们将回调,但JavaThread 不会阻塞。为此,我们为SS::block 添加了一个新的 bool 参数block_in_safepoint_check,默认值为 true。如果该值为 true,我们就会像现在在Safepoint::block() 中一样执行。如果我们调用的 SS::block 值为假,我们就会允许安全点取得进展,但不会尝试抓取Threads_lock。以下是Safepoint::block() 的简略逻辑:

Safepoint_lock->lock_without_safepoint_check();
if (is_synchronizing()) {
 //进行更改以允许安全点继续运行
}
if(block_in_safepoint_check) {
 // 当前逻辑
 thread->set_thread_state(_thread_blocked);
 Safepoint_lock->unlock();
 Threads_lock->lock_without_safepoint_check();

 thread->set_thread_state(state);
 Threads_lock->unlock();
}
else {
 // 我们选择不阻塞
 thread->set_thread_state(_thread_blocked);
 Safepoint_lock->unlock();
}
© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 共1条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片