JVM Bug:多个线程持有一把锁?

JVM线程dump Bug描述

在JAVA语言中,当同步块(Synchronized)被多个线程并发访问时,JVM中会采用基于互斥实现的重量级锁。JVM最多只允许一个线程持有这把锁,如果其它线程想要获得这把锁就必须处于等待状态,也就是说在同步块被并发访问时,最多只会有一个处于RUNNABLE状态的线程持有某把锁,而另外的线程因为竞争不到这把锁而都处于BLOCKED状态。然而有些时候我们会发现处于BLOCKED状态的线程,它的最上面那一帧在打印其正在等待的锁对象时,居然也会出现-locked的信息,这个信息和持有该锁的线程打印出来的结果是一样的(请看下图),但是对比其他BLOCKED态的线程却并没有都出现这种情况。当我们再次dump线程时又可能出现不一样的结果。测试表明这可能是一个偶发的情况,本文就是针对这种情况对JVM内部的实现做了一个研究以寻找其根源。
image.png

JStack命令的整个过程

上面提到了线程dump,那么就不得不提执行线程dump的工具—jstack,这个工具是Java自带的工具,和Java处于同一个目录下,主要是用来dump线程的,或许大家也有使用kill -3的命令来dump线程,但这两者最明显的一个区别是,前者的dump内容是由jstack这个进程来输出的,目标JVM进程将dump内容发给jstack进程(注意这是没有加-m参数的场景,指定-m参数就有点不一样了,它使用的是serviceability agent的api来实现的,底层通过ptrace的方式来获取目标进程的内容,执行过程可能会比正常模式更长点),这意味着可以做文件重定向,将线程dump内容输出到指定文件里;而后者是由目标进程输出的,只会产生在目标进程的标准输出文件里,如果正巧标准输出里本身就有内容的话,看起来会比较乱,比如想通过一些分析工具去分析的话,要是该工具没有做过滤操作,很可能无法分析。因此一般情况我们尽量使用jstack,另外jstack还有很多实用的参数,比如jstack pid >thread_dump.log,该命令会将指定pid的进程的线程dump到当前目录的thread_dump.log文件里。

jstack是使用Java实现的,它通过给目标JVM进程发送一个threaddump的命令,目标JVM的监听线程(attachListener)会实时监听传过来的命令(其实attachListener线程并不是一启动就创建的,它是lazy创建启动的),当attachListener收到threaddump命令时会调用thread_dump的方法来处理dump操作(方法在attachListener.cpp里)。

static jint thread_dump(AttachOperation* op, outputStream* out) {
  bool print_concurrent_locks = false;
  if (op->arg(0) != NULL && strcmp(op->arg(0), "-l") == 0) {
    print_concurrent_locks = true;
  }

  // thread stacks
  VM_PrintThreads op1(out, print_concurrent_locks);
  VMThread::execute(&op1);

  // JNI global handles
  VM_PrintJNI op2(out);
  VMThread::execute(&op2);

  // Deadlock detection
  VM_FindDeadlocks op3(out);
  VMThread::execute(&op3);

  return JNI_OK;
}

从上面的方法可以看到,jstack命令执行了三个操作:

  • VM_PrintThreads:打印线程栈
  • VM_PrintJNI:打印JNI
  • VM_FindDeadlocks:打印死锁

三个操作都是交给VMThread线程去执行的,VMThread线程在整个JAVA进程有且只会有一个。可以想象一下VMThread线程的简单执行过程:不断地轮询某个任务列表并在有任务时依次执行任务。任务执行时,它会根据具体的任务决定是否会暂停整个应用,也就是stop the world,这是不是让我们联想到了我们熟悉的GC过程?是的,我们的ygc以及cmsgc的两个暂停应用的阶段(init_mark和remark)都是由这个线程来执行的,并且都要求暂停整个应用。其实上面的三个操作都是要求暂停整个应用的,也就是说jstack触发的线程dump过程也是会暂停应用的,只是这个过程一般很快就结束,不会有明显的感觉。另外内存dump的jmap命令,也是会暂停整个应用的,如果使用了-F的参数,其底层也是使用serviceability agent的api来dump的,但是dump内存的速度会明显慢很多。

VMThread执行任务的过程

VMThread执行的任务称为vm_opration,在JVM中存在两种vm_opration,一种是需要在安全点内执行的(所谓安全点,就是系统处于一个安全的状态,除了VMThread这个线程可以正常运行之外,其他的线程都必须暂停执行,在这种情况下就可以放心执行当前的一系列vm_opration了),另外一种是不需要在安全点内执行的。而这次我们讨论的线程dump是需要在安全点内执行的。

以下是VMThread轮询的逻辑:

void VMThread::loop() {
  assert(_cur_vm_operation == NULL, "no current one should be executing");

  while(true) {
    ...
    //已经获取了一个vm_operation
    if (_cur_vm_operation->evaluate_at_safepoint()) {
        //如果该vm_operation需要在安全点内执行
        _vm_queue->set_drain_list(safepoint_ops); 
        SafepointSynchronize::begin();//进入安全点
        evaluate_operation(_cur_vm_operation);
        do {
          _cur_vm_operation = safepoint_ops;
          if (_cur_vm_operation != NULL) {
            do {
              VM_Operation* next = _cur_vm_operation->next();
              _vm_queue->set_drain_list(next);
              evaluate_operation(_cur_vm_operation);
              _cur_vm_operation = next;
              if (PrintSafepointStatistics) {
                SafepointSynchronize::inc_vmop_coalesced_count();
              }
            } while (_cur_vm_operation != NULL);
          }
          if (_vm_queue->peek_at_safepoint_priority()) {
            MutexLockerEx mu_queue(VMOperationQueue_lock,
                                     Mutex::_no_safepoint_check_flag);
            safepoint_ops = _vm_queue->drain_at_safepoint_priority();
          } else {
            safepoint_ops = NULL;
          }
        } while(safepoint_ops != NULL);
        _vm_queue->set_drain_list(NULL);
        SafepointSynchronize::end();//退出安全点
      } else {  // not a safepoint operation
        if (TraceLongCompiles) {
          elapsedTimer t;
          t.start();
          evaluate_operation(_cur_vm_operation);
          t.stop();
          double secs = t.seconds();
          if (secs * 1e3 > LongCompileThreshold) {
            tty->print_cr("vm %s: %3.7f secs]", _cur_vm_operation->name(), secs);
          }
        } else {
            evaluate_operation(_cur_vm_operation);
        }
        _cur_vm_operation = NULL;
      }
    }
    ...
  }

在这里重点解释下在安全点内执行的vm_opration的过程,VMThread通过不断循环从_vm_queue中获取一个或者几个需要在安全点内执行的vm_opertion,然后在准备执行这些vm_opration之前先通过调用SafepointSynchronize::begin()进入到安全点状态,在执行完这些vm_opration之后,调用SafepointSynchronize::end(),退出安全点模式,恢复之前暂停的所有线程让他们继续运行。对于安全点这块的逻辑挺复杂的,仅仅需要记住在进入安全点模式的时候会持有Threads_lock这把线程互斥锁,对线程的操作都需要获取到这把锁才能继续执行,并且还会设置安全点的状态,如果正在进入安全点过程中设置_state为_synchronizing,当所有线程都完全进入了安全点之后设置_state为_synchronized状态,退出的时候设置为_not_synchronized状态。

void SafepointSynchronize::begin() {
  ...
  Threads_lock->lock();
  ...
  _state            = _synchronizing;
  ...
   _state = _synchronized;
...
}

void SafepointSynchronize::end() {
    assert(Threads_lock->owned_by_self(), "must hold Threads_lock");
    ...
    _state = _not_synchronized;
    ...
    Threads_lock->unlock();
}

线程Dump中的VM_PrintThreads过程

回到开头提到的JVM线程Dump时的Bug,从我们打印的结果来看也基本猜到了这个过程:遍历每个Java线程,然后再遍历每一帧,打印该帧的一些信息(包括类,方法名,行数等),在打印完每一帧之后然后打印这帧已经关联了的锁信息,下面代码就是打印每个线程的过程:

void JavaThread::print_stack_on(outputStream* st) {
   
  if (!has_last_Java_frame()) return;
  ResourceMark rm;
  HandleMark   hm;

  RegisterMap reg_map(this);
  vframe* start_vf = last_java_vframe(®_map);
  int count = 0;
  for (vframe* f = start_vf; f; f = f->sender() ) {
   
    if (f->is_java_frame()) {
   
      javaVFrame* jvf = javaVFrame::cast(f);
      java_lang_Throwable::print_stack_element(st, jvf->method(), jvf->bci());
      if (JavaMonitorsInStackTrace) {
   
        jvf->print_lock_info_on(st, count);
      }
    } else {
   
      // Ignore non-Java frames
    }
    count++;
    if (MaxJavaStackTraceDepth == count) return;
  }
}

和我们这次问题相关的逻辑,也就是打印"-locked"的信息是正好是在jvf->print_lock_info_on(st, count)这行里面,请看具体实现:

void javaVFrame::print_lock_info_on(outputStream* st, int frame_count) {
   
  ResourceMark rm;
  if (frame_count == 0) {
   
    if (method()->name() == vmSymbols::wait_name() &&
        instanceKlass::cast(method()->method_holder())->name() == vmSymbols::java_lang_Object()) {
   
      StackValueCollection* locs = locals();
      if (!locs->is_empty()) {
   
        StackValue* sv = locs->at(0);
        if (sv->type() 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HeapDump性能社区

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值