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

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 收集器工作流程)