美文网首页
GC part 1:GC是怎么开始工作的

GC part 1:GC是怎么开始工作的

作者: 一字马胡 | 来源:发表于2021-08-02 11:03 被阅读0次

part 1

首选想探索一下GC是怎么开始工作的,或者说,GC到底是以什么样的方式在工作的;java应用在启动的时候会创建一个jvm进程,JVM内部通过调用create_vm来实现,该方法做了大量的工作来创建一个jvm进程,并且将java应用的main方法启动起来,运行在main线程中(主线程);在create_vm中,有一个地方值得关注,下面是thread.cpp中create_vm方法的代码片段:

  // Create the VMThread
  { TraceTime timer("Start VMThread", TRACETIME_LOG(Info, startuptime));

  VMThread::create();
    Thread* vmthread = VMThread::vm_thread();

    if (!os::create_thread(vmthread, os::vm_thread)) {
      vm_exit_during_initialization("Cannot create VM thread. "
                                    "Out of system resources.");
    }

    // Wait for the VM thread to become ready, and VMThread::run to initialize
    // Monitors can have spurious returns, must always check another state flag
    {
      MutexLocker ml(Notify_lock);
      os::start_thread(vmthread);
      while (vmthread->active_handles() == NULL) {
        Notify_lock->wait();
      }
    }
  }

VMThread是一种特殊的jvm线程,用于执行比如GC等操作,java代码的Thread和JVM里面的JavaThread对应,这一点后续再研究;上面的代码片段首先关注【VMThread::create()】这个函数调用,在VMThread.cpp中实现了该函数:


void VMThread::create() {
  assert(vm_thread() == NULL, "we can only allocate one VMThread");
  _vm_thread = new VMThread();

  // Create VM operation queue
  _vm_queue = new VMOperationQueue();
  guarantee(_vm_queue != NULL, "just checking");

  _terminate_lock = new Monitor(Mutex::safepoint, "VMThread::_terminate_lock", true,
                                Monitor::_safepoint_check_never);

  if (UsePerfData) {
    // jvmstat performance counters
    Thread* THREAD = Thread::current();
    _perf_accumulated_vm_operation_time =
                 PerfDataManager::create_counter(SUN_THREADS, "vmOperationTime",
                                                 PerfData::U_Ticks, CHECK);
  }
}

create函数在new了一个VMThread对象实例同时,为该VMThread创建了一个VMOperationQueue,VMThread有一个重要的成员叫_vm_queue,看看它的定义:


  static VMOperationQueue* _vm_queue;           // Queue (w/ policy) of VM operations

根据注释可以将该queue理解为是VMThread的任务队列,但是队列内部存放的任务都是VMOperation,不能是其他类型的任务,那VMOperation是什么呢?其实有一个基类叫VM_Operation,有一个子类叫VM_GC_Operation,就是专门来做GC的任务,在对象申请内存分配失败的时候会生成一个VM_CollectForAllocation任务来做GC,_vm_queue队列就是用来存储这些任务的,VMThread会不断来check该队列是否有任务需要执行,这种工作模式类似于特殊的线程池,这个线程池只有一个VMThread,_vm_queue就是线程池中的任务队列;
VMThread创建完成之后,create_vm函数将等到VMThread启动成功,判断VMThread是否已经正常工作的标准是vmthread->active_handles() == NULL为true,也就是执行了VMThread的run函数,这里说明一下,JVM的线程实现方法是将一个os线程绑定到JVM线程上,所以每创建一个JVM线程都需要创建一个os线程来做绑定,不同环境下创建os线程的方法不一样,比如在Mac下,就是使用bsd的方法来创建os线程的;回头看create_vm函数里面的那段代码片段,可以看到使用了os::create_thread(vmthread, os::vm_thread)来创建了一个os线程,在os_bsd.cpp内部的create_thread函数里面,可以看到创建了一个os线程,并且将该os线程绑定到了创建好的VMThread上,几乎在所有的os上创建线程的同时需要指定一个方法入口,使得os在创建好了线程之后可以准备执行相应的代码,可以在create_thread方法里面看到下面的代码片段:

  pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);

thread_native_entry就是上面提到的代码入口,可以在thread_native_entry函数内部看到执行了VMThread的run方法,到此create_vm函数可以继续执行;
接下来再回过头来看看VMThread的run函数,该函数执行了一些线程初始化的工作,比如设置线程名称,线程优先级等,然后执行了一个关键的方法:loop,该方法可以理解为VMThread将不断轮询来从自己的任务队列_vm_queue中获取任务来执行,下面来仔细研究一下loop函数的关键步骤。

  • (1)、通过remove_next方法获取任务,如果当前任务队列中没有待执行的任务,那么remove_next函数会返回NULL,下面是remove_next函数的具体实现
VM_Operation* VMOperationQueue::remove_next() {
  // Assuming VMOperation queue is two-level priority queue. If there are
  // more than two priorities, we need a different scheduling algorithm.
  assert(SafepointPriority == 0 && MediumPriority == 1 && nof_priorities == 2,
         "current algorithm does not work");

  // simple counter based scheduling to prevent starvation of lower priority
  // queue. -- see 4390175
  int high_prio, low_prio;
  if (_queue_counter++ < 10) {
      high_prio = SafepointPriority;
      low_prio  = MediumPriority;
  } else {
      _queue_counter = 0;
      high_prio = MediumPriority;
      low_prio  = SafepointPriority;
  }

  return queue_remove_front(queue_empty(high_prio) ? low_prio : high_prio);
}

VM_Operation* VMOperationQueue::queue_remove_front(int prio) {
  if (queue_empty(prio)) return NULL;
  assert(_queue_length[prio] >= 0, "sanity check");
  _queue_length[prio]--;
  VM_Operation* r = _queue[prio]->next();
  assert(r != _queue[prio], "cannot remove base element");
  unlink(r);
  return r;
}
  • (2)、如果发现任务队列中没有待执行的任务,那么VMThread不能一直傻傻的轮询啊,就会让自己进入等待状态
image
  • (3)、在(2)步骤中等待多时之后,VMThread可能会被一些任务填充线程唤醒(notify),这个时候loop函数就会继续执行接下来的代码,有一些Operation任务要求在safe_point执行,比如FullGC,使用SafepointSynchronize::begin()和SafepointSynchronize::end()可以达到这个目的,就像下面这样:
SafepointSynchronize::begin()

/// safe point code 

SafepointSynchronize::end();

无论如何,接下来就是要执行队列中取出来的任务了,所以evaluate_operation(_cur_vm_operation)方法应该是我们接下来应该关注的;在evaluate_operation函数内部看到了调用了evaluate()函数,接着看看evaluate函数;

void VM_Operation::evaluate() {
  ResourceMark rm;
  outputStream* debugstream;
  bool enabled = log_is_enabled(Debug, vmoperation);
  if (enabled) {
    debugstream = Log(vmoperation)::debug_stream();
    debugstream->print("begin ");
    print_on_error(debugstream);
    debugstream->cr();
  }
  doit();
  if (enabled) {
    debugstream->print("end ");
    print_on_error(debugstream);
    debugstream->cr();
  }
}

关键的是doit()函数,这里面就是具体的任务执行内容,不同的Operation的doit内容都是不一样的,就算是GC_Opertion,还是有多种不同的方式的,比如上面提到了VM_GenCollectForAllocation的doit内容做的工作就是这样的:

void VM_GenCollectForAllocation::doit() {
  SvcGCMarker sgcm(SvcGCMarker::MINOR);

  GenCollectedHeap* gch = GenCollectedHeap::heap();
  GCCauseSetter gccs(gch, _gc_cause);
  _result = gch->satisfy_failed_allocation(_word_size, _tlab);
  assert(gch->is_in_reserved_or_null(_result), "result not in heap");

  if (_result == NULL && GCLocker::is_active_and_needs_gc()) {
    set_gc_locked();
  }
}

gch->satisfy_failed_allocation就是为了解决空间分配失败的,去看satisfy_failed_allocation函数的注释,可以看到:

  // Callback from VM_GenCollectForAllocation operation.
  // This function does everything necessary/possible to satisfy an
  // allocation request that failed in the youngest generation that should
  // have handled it (including collection, expansion, etc.)
  HeapWord* satisfy_failed_allocation(size_t size, bool is_tlab);

这个函数会被VM_GenCollectForAllocation执行的时候回调,也就是doit函数执行的时候调用这个函数,这个函数会做类似于垃圾收集,堆扩展等工作来满足一个"allocation request";当然,回调这个函数之前必然已经尝试进行空间分配申请了,并且已经失败了,所以该函数需要极尽所能去做工作来腾出空间(申请新的空间)来满足已经失败的空间分配申请;collectorPolicy类实现了垃圾收集的策略,所谓垃圾收集策略就是应该在什么时候做GC,做什么类型的GC等,参考价值很大;下面可以试着来看一下satisfy_failed_allocation函数具体是怎么做的;

image

从上面这张图可以看到,如果发现gc_lock是活动的,也就说明已经有其他的线程触发了GC,那么这个时候策略就是扩展堆来满足内存申请。

image

看if条件,如果增量GC是安全的,那么就执行增量安全,所谓增量GC,就是按照从轻到重的程度来做垃圾回收,大概分这么几个级别,首先是进行一次MinorGC,其次是进行一次FullGC,最后是进行一次带soft reference清理的FullGC;上面的图片对应的是第一种情况,进行一次MinorGC,然后尝试申请空间,如果成功就打住了,否则就要进行一次清理soft reference的FullGC了,硕大soft reference,可以大概说一下,java中的引用分四个级别,strong reference > soft reference > weak reference > phantom reference;强度梯度下降,strong reference只要对象还在被引用就不会被回收,而soft reference就不一样了,JVM在尝试进行GC来解决内存不足的状况下,如果发现还是无法满足内存申请,那么就会将这部分引用类型的对象回收回来,所以,在使用soft reference的时候不应该强依赖于对象,因为不知道什么时候就被回收了,这种引用可以用在缓存的场景中;weak reference的强度比soft弱一些,它只能存活到下次GC发生,而phantom reference就更弱了,弱到你根本无法获取到一个phantom reference对象,它唯一的作用就是可以在发生GC的时候告诉你它已经被回收了;下面的代码展示了进行FullGC的两种情况:

image image

** 总结一下再allocate fail的应对策略,首先判断是否有其他线程触发了GC操作,如果是的话则不会进行GC操作,而是尝试去扩展堆来解决allocate fail,否则判断是否可以进行增量GC,如果可以,那么执行一次MinorGC,否则执行一次不回收soft reference的FullGC,之后判断是否可以解决allocate fail了,如果可以了就到此打住,否则进行一次彻底的FullGC,也就是将soft reference也回收回来 **

其实在(3)的时候,漏掉一个细节,VMThread会将任务队列中填充好的任务都执行完成,才会继续执行接下来的代码;最后希望能看一下到底在什么地方会将任务填充到VMThread的任务队列中去;还是拿VM_GenCollectForAllocation来说,可以在collectorPolicy.cpp中的mem_allocate_work看到执行了类似下面的代码:

    VM_GenCollectForAllocation op(size, is_tlab, gc_count_before);
    VMThread::execute(&op);

然后又回来VMThread看execute方法,可以看到下面的细节:

image

到此,GC任务是怎么运行的大概梳理了一下,具体的GC细节还是需要再梳理。

相关文章

  • GC part 1:GC是怎么开始工作的

    part 1 首选想探索一下GC是怎么开始工作的,或者说,GC到底是以什么样的方式在工作的;java应用在启动的时...

  • HotSpot note (part-1)

    part 1 首选想探索一下GC是怎么开始工作的,或者说,GC到底是以什么样的方式在工作的;java应用在启动的时...

  • HotSpot note (part-3)

    part 3 DefNew的GC属于Minor GC,使用copying算法进行垃圾收集,是Serial GC(-...

  • GC part 3

    part 3 DefNew的GC属于Minor GC,使用copying算法进行垃圾收集,是Serial GC(-...

  • DefNewGeneration 之二

    1、gc_prologue / gc_epiloguegc_prologue方法是在GC开始前调用的预处理,gc_...

  • HotSpot note(part-5)

    part 5 本comment希望能系统的探索一下GC发生的时机,以及各个GC的具体工作内容(流程),GC包括Mi...

  • GC part 5

    part 5 本comment希望能系统的探索一下GC发生的时机,以及各个GC的具体工作内容(流程),GC包括Mi...

  • Go GC

    1、什么是GC?2、为什么会有GC?3、GC的优点?4、GC的缺点?5、Go中的GC历史6、Go中的GC实现原理(...

  • 后端调优基础——GC调优

    GC类型 Minor GC :清理新生代,Minor GC是最频繁触发的GC,速度也最快的,主要工作原理是:对象在...

  • java常见垃圾收集器

    常见的有Serial GC、ParNew GC、CMS GC、Parallel GC、G1 GC Serial G...

网友评论

      本文标题:GC part 1:GC是怎么开始工作的

      本文链接:https://www.haomeiwen.com/subject/ghwdvltx.html