美文网首页程序员技术栈
JVM源码分析(四)Parralel Scavenge 收集器工

JVM源码分析(四)Parralel Scavenge 收集器工

作者: msrpp | 来源:发表于2018-12-24 20:31 被阅读2次

    Parralel Scavenge 收集器工作流程

    jvm初始化的时候,有个重要的步骤是全局堆的初始化,根据vm参数的不同,又会选择不同的堆实现(堆的实现在share/vm/memory中,策略选择位于share/vm/gc_implementation, 详情见Universe::initialize_heap())。server模式下启动的jvm,默认采用的全局堆实现是ParallelScavengeHeap,本文记录了jdk1.8版本的Parralel Scavenge实现,很多细节还不清楚。

    一、PS收集器概览及初始化

    ParallelScavengeHeap由两个区域组成,_old_gen和_young_gen,其中_young_gen又由eden,from,to三个MutableSpace组成。大致如下:

    image.png

    我们可以用:

    • -xmn500m 来指定新时代的大小为500m
    • -XX:SurvivorRatio=8 来指定eden区和(from+to)区占比8:1.
    • -XX:MaxTenuringThreshold=18 来指定晋升年龄

    新生代的3个区域是连续的空间,做法是开辟一个虚拟内存,根据起始大小计算分配出eden,两个survivor区域。虚拟内存不会立马占用物理内存,每次分配对象时载入物理内存。在首次将新生代数据晋升到年老代时物理内存会急剧升高,(测试:在指定xmn大小为500m的情况下,每次new 4m大小空间,约400m时触发一次 minor gc,内存突然增长到750m)。

    java的new关键字实际会作用到jvm的申请内存操作(声明的对象名称会在栈内分配),调用到interpreterRuntime.cpp中的InterpreterRuntime::_new方法,为了方便触发gc,我用了new大数组的方式来调试。
    创建数组的入口函数有两个,一个是用于基本类型的内存申请, 另一个是对象数组的。

    IRT_ENTRY(void, InterpreterRuntime::newarray(JavaThread* thread, BasicType type, jint size))
      oop obj = oopFactory::new_typeArray(type, size, CHECK);
      thread->set_vm_result(obj);
    IRT_END
    
    
    IRT_ENTRY(void, InterpreterRuntime::anewarray(JavaThread* thread, ConstantPool* pool, int index, jint size))
      // Note: no oopHandle for pool & klass needed since they are not used
      //       anymore after new_objArray() and no GC can happen before.
      //       (This may have to change if this code changes!)
      Klass*    klass = pool->klass_at(index, CHECK);
      objArrayOop obj = oopFactory::new_objArray(klass, size, CHECK);
      thread->set_vm_result(obj);
    IRT_END
    
    

    申请内存需要经过以下几个步骤。

    • 1.尝试从tlab中分配内存,分配成功直接返回;不成功进入下一步。

    • 2.从垃圾收集器中分配内存,如果没有相关指定收集器的配置,Server模式默认的是parralel Scavenge的方式分配。

    • 3.从新生代中分配,关键代码HeapWord* result = young_gen()->allocate(size);,我们知道新生代实际直接存放新建对象的区域是eden区,关键代码HeapWord* result = eden_space()->cas_allocate(word_size);

    • 4.内存的实际分配是MutableSpace类实现的,判断当前剩余空闲内存是否足够放下申请的对象,如果可以,那么成功返回。如果不行,进入步骤5。

    • 5.如果对象的大小大于eden区的一半,那么直接分配到老年代中去,否则进入下一步。

    
    HeapWord* ParallelScavengeHeap::mem_allocate_old_gen(size_t size) {
      if (!should_alloc_in_eden(size) || GC_locker::is_active_and_needs_gc()) {
        // Size is too big for eden, or gc is locked out.
        return old_gen()->allocate(size);
      }
    
      // If a "death march" is in progress, allocate from the old gen a limited
      // number of times before doing a GC.
      if (_death_march_count > 0) {
        if (_death_march_count < 64) {
          ++_death_march_count;
          return old_gen()->allocate(size);
        } else {
          _death_march_count = 0;
        }
      }
      return NULL;
    }
    
    inline bool ParallelScavengeHeap::should_alloc_in_eden(const size_t size) const
    {
      const size_t eden_size = young_gen()->eden_space()->capacity_in_words();
      return size < eden_size / 2;
    }
    
    
    • 6.触发一个VM_ParallelGCFailedAllocation任务抛给vm线程,这里注意下VMThread的execute方法是阻塞的,需要等到vm线程的gc任务完成当前线程才会返回。默认的任务是VM_ParallelGCFailedAllocation。查看其doit方法
    
    void VM_ParallelGCFailedAllocation::doit() {
      SvcGCMarker sgcm(SvcGCMarker::MINOR);
    
      ParallelScavengeHeap* heap = (ParallelScavengeHeap*)Universe::heap();
      assert(heap->kind() == CollectedHeap::ParallelScavengeHeap, "must be a ParallelScavengeHeap");
    
      GCCauseSetter gccs(heap, _gc_cause);
      _result = heap->failed_mem_allocate(_word_size);
    
      if (_result == NULL && GC_locker::is_active_and_needs_gc()) {
        set_gc_locked();
      }
    }
    
    
    HeapWord* ParallelScavengeHeap::failed_mem_allocate(size_t size) {
      assert(SafepointSynchronize::is_at_safepoint(), "should be at safepoint");
      assert(Thread::current() == (Thread*)VMThread::vm_thread(), "should be in vm thread");
      assert(!Universe::heap()->is_gc_active(), "not reentrant");
      assert(!Heap_lock->owned_by_self(), "this thread should not own the Heap_lock");
    
      // We assume that allocation in eden will fail unless we collect.
    
      // First level allocation failure, scavenge and allocate in young gen.
      GCCauseSetter gccs(this, GCCause::_allocation_failure);
      const bool invoked_full_gc = PSScavenge::invoke();
      HeapWord* result = young_gen()->allocate(size);
    
      // Second level allocation failure.
      //   Mark sweep and allocate in young generation.
      if (result == NULL && !invoked_full_gc) {
        do_full_collection(false);
        result = young_gen()->allocate(size);
      }
    
      death_march_check(result, size);
    
      // Third level allocation failure.
      //   After mark sweep and young generation allocation failure,
      //   allocate in old generation.
      if (result == NULL) {
        result = old_gen()->allocate(size);
      }
    
      // Fourth level allocation failure. We're running out of memory.
      //   More complete mark sweep and allocate in young generation.
      if (result == NULL) {
        do_full_collection(true);
        result = young_gen()->allocate(size);
      }
    
      // Fifth level allocation failure.
      //   After more complete mark sweep, allocate in old generation.
      if (result == NULL) {
        result = old_gen()->allocate(size);
      }
    
      return result;
    }
    
    
    • 7.可以看到,先执行一次minor gc,然后尝试在新生代分配。如果不成功且minor gc执行过程中没有去full gc,那么要来一次fullgc,尝试从新生代中分配。 还不成功,再来一次fullgc(带清楚软引用的),从新生代中分配。依然不成功,从老年代分配。还不成功?OOM吧。。

    重点是const bool invoked_full_gc = PSScavenge::invoke();贴一下实现,先执行一次minor gc。不成功的话,触发fullgc。fullgc根据配置有两种选择,PSMarkSweep和PSParallel。没有CMS,因为Parraler Scavenge收集器和他无法同时使用。

    bool PSScavenge::invoke() {
      assert(SafepointSynchronize::is_at_safepoint(), "should be at safepoint");
      assert(Thread::current() == (Thread*)VMThread::vm_thread(), "should be in vm thread");
      assert(!Universe::heap()->is_gc_active(), "not reentrant");
    
      ParallelScavengeHeap* const heap = (ParallelScavengeHeap*)Universe::heap();
      assert(heap->kind() == CollectedHeap::ParallelScavengeHeap, "Sanity");
    
      PSAdaptiveSizePolicy* policy = heap->size_policy();
      IsGCActiveMark mark;
    
      const bool scavenge_done = PSScavenge::invoke_no_policy();
      const bool need_full_gc = !scavenge_done ||
        policy->should_full_GC(heap->old_gen()->free_in_bytes());
      bool full_gc_done = false;
    
      if (UsePerfData) {
        PSGCAdaptivePolicyCounters* const counters = heap->gc_policy_counters();
        const int ffs_val = need_full_gc ? full_follows_scavenge : not_skipped;
        counters->update_full_follows_scavenge(ffs_val);
      }
    
      if (need_full_gc) {
        GCCauseSetter gccs(heap, GCCause::_adaptive_size_policy);
        CollectorPolicy* cp = heap->collector_policy();
        const bool clear_all_softrefs = cp->should_clear_all_soft_refs();
    
        if (UseParallelOldGC) {
          full_gc_done = PSParallelCompact::invoke_no_policy(clear_all_softrefs);
        } else {
          full_gc_done = PSMarkSweep::invoke_no_policy(clear_all_softrefs);
        }
      }
    
      return full_gc_done;
    }
    

    接下来进入重点了,minor gc的具体实现在PSScavenge::invoke_no_policy,代码太长不贴了。简单说下流程。

    • 1.sanity check,记录gc前的内存信息。

    • 2.向gc worker线程池投递gc任务,根据根节点搜索法保存有效的对象。根节点有很多种,如下。注意这些gc任务是并发执行的。

          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::universe));
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::jni_handles));
          // We scan the thread roots in parallel
          Threads::create_thread_roots_tasks(q);
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::object_synchronizer));
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::flat_profiler));
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::management));
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::system_dictionary));
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::class_loader_data));
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::jvmti));
          q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::code_cache));
    

    可达性分析算法实现都差不多,步骤是遍历根节点的所有可达对象,判断对象是不是在新生代(根据内存地址判断),是的话调用PSPromotionManager::copy_to_survivor_space转移存活对象,如果指定了需要晋升或者对象的年龄(新生代对象每经过一次minor gc加1岁)达到阙值,则拷贝到old区,否则拷贝到to区。

    • 3.清空eden区和from区,再将有存活的对象的to区和from区做交换(保证存活对象放在from区,这样下次gc的时候就可以再次执行复制算法将对象拷贝到to区了)。

      1. 调用resize_young_gen方法重新分配新时代大小(疑问:看注释是minor gc会引起新生代大小的变化,具体什么情况?)
    • 4.如果有对象晋升失败了,那么可能是old区空间不足了,此时需要触发一次full gc,如果老年代的策略是ps old,那么处理老年代的gc由类PSParallelCompact,这一段后续分析。

    相关文章

      网友评论

        本文标题:JVM源码分析(四)Parralel Scavenge 收集器工

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