JVM之对象分配(G1)

概述

在JDK1.9中移除了JDK 8中已弃用的垃圾收集器(GC)组合,官网原始文档如下:

Removes garbage collector (GC) combinations that were deprecated in JDK 8.
This means that the following GC combinations no longer exist:

  1. DefNew + CMS
  2. ParNew + SerialOld
  3. Incremental CMS

The "foreground" mode for Concurrent Mark Sweep (CMS) has also been
removed. The following command-line flags have been removed:

  1. -Xincgc
  2. -XX:+CMSIncrementalMode
  3. -XX:+UseCMSCompactAtFullCollection
  4. -XX:+CMSFullGCsBeforeCompaction
  5. -XX:+UseCMSCollectionPassing

The command line flag -XX:+UseParNewGC no longer has an effect. ParNew can only be used with CMS and CMS requires ParNew. Thus, the -XX:+UseParNewGC flag has been deprecated and will likely be removed in a future release.

在JDK1.9以后,将不再推荐使用CMS垃圾回收器,官方推荐使用 G1 垃圾回收器,官方原始文档如下:

Makes Garbage-First (G1) the default garbage collector (GC) on 32- and 64-bit >server configurations. Using a low-pause collector such as G1 provides a better >overall experience, for most users, than a throughput-oriented collector such as >the Parallel GC, which was previously the default.

由此可见,在未来G1将成为JVM虚拟机中主流的垃圾回收器,接下来我们去了解一下在使用 G1 的情况下,对象的分配流程。

对象分配通有流程

对象的分配流程如下图:


  1. 判断对象是否已经加载,如果已经加载,执行步骤 3,否则执行步骤 2
  2. 通过类加载器加载类的字节码文件,对字节码文件进行验证、准备、解析等操作,进入步骤 3
  3. 为对象分配内存空间,首先尝试在TLAB空间分配对象,如果分配失败,在eden空间分配
  4. 在eden空间分配对象,如果分配成功则对对象进行初始化,并将对象引用赋给变量,如果分配失败则触发GC,进行垃圾回收,对内存空间进行回收以后,重新尝试对象的分配。

以上是对象分配的通有流程,但是在采用不同垃圾回收器的情况下,对象的分配流程有所差异,下面我们来研究一下在使用G1的情况下,对象分配的流程是怎么样的。

G1对象分配

程序启动时,使用模板解释器来执行字节码,当执行new操作时,调用下面代码完成对象创建工作,源码地址:openjdk\hotspot\src\share\vm\interpreter\interpreterRuntime.cpp

IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, constantPoolOopDesc* pool, int index))

  //从运行时常量池中获取KlassOop
  klassOop k_oop = pool->klass_at(index, CHECK);
  instanceKlassHandle klass (THREAD, k_oop);

  // 确保我们没有实例化一个抽象的klass
  klass->check_valid_for_instantiation(true, CHECK);

  // 保证已经完成类加载和初始化
  klass->initialize(CHECK);

  //分配对象
  oop obj = klass->allocate_instance(CHECK);
  thread->set_vm_result(obj);
IRT_END

在创建对象之前,从运行时常量池中获取 Klass 对象,调用check_valid_for_instantiation 方法,确保我们初始化的类不是一个抽象类,接下来检查对象是否已经加载(如果未加载,通过类加载器加载class),最后调用allocate_instance 方法,创建对象,接下来我们深入进去,查看源码具体实现。源码地址:openjdk\hotspot\src\share\vm\oops\instanceKlass.cpp

instanceOop instanceKlass::allocate_instance(TRAPS) {
  assert(!oop_is_instanceMirror(), "wrong allocation path");
  //是否重写finalize()方法
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  //分配的对象的大小
  int size = size_helper();  // Query before forming handle.
  KlassHandle h_k(THREAD, as_klassOop());

  instanceOop i;

  //分配对象
  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

通过调用 instanceKlass::allocate_instance 方法,分配对象:首先判断类是否重写finalize()方法,获取对象需要分配的内存空间的大小,然后创建对象,如果之前判断对象重写了finalize()方法,将该对象注册到 Finalizer 队列里面。在该方法中调用CollectedHeap::obj_allocate方法创建对象,继续往下深入,源码地址:openjdk\hotspot\src\share\vm\gc_interface\collectedHeap.inline.hpp

//对象内存空间分配
oop CollectedHeap::obj_allocate(KlassHandle klass, int size, TRAPS) {
  debug_only(check_for_valid_allocation_state());
  //校验在GC的时候不分配内存
  assert(!Universe::heap()->is_gc_active(), "Allocation during gc not allowed");
  //分配大小大于0
  assert(size >= 0, "int won't convert to size_t");
  //内存分配
  HeapWord* obj = common_mem_allocate_init(klass, size, CHECK_NULL);
  //初始化
  post_allocation_setup_obj(klass, obj);
  NOT_PRODUCT(Universe::heap()->check_for_bad_heap_word_value(obj, size));
  return (oop)obj;
}

在上面的代码中,通过断言判断现在是否在执行GC,以及对象分配的大小是否大于0,如果断言通过,进行内存空间的分配,然后对对象进行初始化操作。关于对象的内存分配主要时调用CollectedHeap类的 common_mem_allocate_init 方法,代码如下:

HeapWord* CollectedHeap::common_mem_allocate_init(KlassHandle klass, size_t size, TRAPS) {
  //申请内存
  HeapWord* obj = common_mem_allocate_noinit(klass, size, CHECK_NULL);
  //字节填充对齐
  init_obj(obj, size);
  return obj;
}

从上面的代码中可以看出,主要是调用 common_mem_allocate_noinit 方法分配了内存空间,然后调用 init_obj 方法对分配的内存进行填充。继续深入到 common_mem_allocate_noinit方法中,查看内存分配的详情。

HeapWord* CollectedHeap::common_mem_allocate_noinit(KlassHandle klass, size_t size, TRAPS) {
  .............................
  HeapWord* result = NULL;
  if (UseTLAB) {//在TLAB中分配
    result = allocate_from_tlab(klass, THREAD, size);
    if (result != NULL) {
      assert(!HAS_PENDING_EXCEPTION,
             "Unexpected exception, will result in uninitialized storage");
      return result;
    }
  }
  bool gc_overhead_limit_was_exceeded = false;
  //在堆中分配
  result = Universe::heap()->mem_allocate(size,
                                          &gc_overhead_limit_was_exceeded);
  if (result != NULL) {
    NOT_PRODUCT(Universe::heap()->
      check_for_non_bad_heap_word_value(result, size));
    assert(!HAS_PENDING_EXCEPTION,
           "Unexpected exception, will result in uninitialized storage");
    THREAD->incr_allocated_bytes(size * HeapWordSize);
    AllocTracer::send_allocation_outside_tlab_event(klass, size * HeapWordSize);

    return result;
  }
    ..............................
    //抛出异常
    THROW_OOP_0(Universe::out_of_memory_error_gc_overhead_limit());
  }
}

上面贴了部分关键代码,首先对象会尝试在TLAB空间分配对象,如果分配失败,则尝试在Eden空间分配内存,如果在Eden空间也分配失败,就会抛出常见的OutOfMemoryError的异常,下面分别讲解如何在TLAB空间和Eden空间划分内存空间。

G1内存结构

在讲对象如何在TLAB空间和Eden空间分配之前,我们先了解一下使用G1垃圾回收器时,内存结构是什么样的。G1内存结构图如下:

在使用G1垃圾收集器的情况下,堆被划分为多个内存大小相等的Region,每个Region被划分到不同的年龄代中,下面我们结合源码,去查看一下在使用G1的情况下,Region的空间大小。源码地址:hotspot\src\share\vm\gc_implementation\g1\heapRegion.cpp

//最小region大小
#define MIN_REGION_SIZE  (      1024 * 1024 )
//最大region大小
#define MAX_REGION_SIZE  ( 32 * 1024 * 1024 )
//region个数
#define TARGET_REGION_NUMBER          2048

void HeapRegion::setup_heap_region_size(uintx min_heap_size) {
  // region_size in bytes
  uintx region_size = G1HeapRegionSize;
  if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {
    region_size = MAX2(min_heap_size / TARGET_REGION_NUMBER,
                       (uintx) MIN_REGION_SIZE);
  }

  int region_size_log = log2_long((jlong) region_size);
  //region_size是2的指数
  region_size = ((uintx)1 << region_size_log);

  // Now make sure that we don't go over or under our limits.
  if (region_size < MIN_REGION_SIZE) {
    region_size = MIN_REGION_SIZE;
  } else if (region_size > MAX_REGION_SIZE) {
    region_size = MAX_REGION_SIZE;
  }

  // And recalculate the log.
  region_size_log = log2_long((jlong) region_size);
  ....................................省略..........................................
}

从上面的代码可以看出,G1默认region的最小大小为1M,最大大小为32M,且每个region的大小是2^n次,同时默认将堆划分为2048块。

TLAB空间对象分配

上面小节,我们分析了G1的内存结构,这小节我们分析对象如何在TLAB空间分配。通过调用 allocate_from_tlab 方法,实现在TLAB空间分配对象,代码如下:

HeapWord* CollectedHeap::allocate_from_tlab(KlassHandle klass, Thread* thread, size_t size) {
  assert(UseTLAB, "should use UseTLAB");
  //TLAB分配
  HeapWord* obj = thread->tlab().allocate(size);
  if (obj != NULL) {
    return obj;
  }
  // Otherwise..
  //慢分配
  return allocate_from_tlab_slow(klass, thread, size);
}

在上面的代码中,先尝试在当前线程指向的TLAB空间分配(通过指针碰撞算法分配对象),如果对象分配成功,则返回对象,如果分配失败,则进入慢分配流程。接下来我们分析一下慢分配流程:


HeapWord* CollectedHeap::allocate_from_tlab_slow(KlassHandle klass, Thread* thread, size_t size) {
  //TLAB空间剩余大小大于可忽略大小
  if (thread->tlab().free() > thread->tlab().refill_waste_limit()) {
    //重设可忽略大小
    thread->tlab().record_slow_allocation(size);
    return NULL;
  }
  //计算新TLAB空间大小
  size_t new_tlab_size = thread->tlab().compute_size(size);
  
  thread->tlab().clear_before_allocation();

  if (new_tlab_size == 0) {
    return NULL;
  }

  // 分配新的TLAB空间
  HeapWord* obj = Universe::heap()->allocate_new_tlab(new_tlab_size);
  if (obj == NULL) {
    return NULL;
  }
  ........................省略..........................
  //分配对象
  thread->tlab().fill(obj, obj + size, new_tlab_size);
  return obj;
}

在上面的代码中,先判断TLAB空间剩余的大小是否大于可忽略的大小,如果满足,就重新设置可忽略大小,如果不满足,重新申请一片TLAB空间,进行对象分配,在使用G1垃圾回收器的情况下,申请新TLAB空间的具体实现如下,源码地址:hotspot\src\share\vm\gc_implementation\g1\g1CollectedHeap.cpp

HeapWord* G1CollectedHeap::allocate_new_tlab(size_t word_size) {
  assert_heap_not_locked_and_not_at_safepoint();
  assert(!isHumongous(word_size), "we do not allow humongous TLABs");

  unsigned int dummy_gc_count_before;
  return attempt_allocation(word_size, &dummy_gc_count_before);
}

上面的代码中主要是调用 attempt_allocation 方法分配内存空间,继续深入查看代码

G1CollectedHeap::attempt_allocation(size_t word_size,
                                    unsigned int* gc_count_before_ret) {
  assert_heap_not_locked_and_not_at_safepoint();
  assert(!isHumongous(word_size), "attempt_allocation() should not "
         "be called for humongous allocation requests");
  //_mutator_alloc_region内部持有一个引用_alloc_region,指向当前正活跃的eden region
  HeapWord* result = _mutator_alloc_region.attempt_allocation(word_size,
  
  //通过CAS分配对象失败,通过加锁分配内存                                                    false /* bot_updates */);
  if (result == NULL) {
    result = attempt_allocation_slow(word_size, gc_count_before_ret);
  }
  assert_heap_not_locked();
  if (result != NULL) {
    dirty_young_block(result, word_size);
  }
  return result;
}

从上面可以看出,通过 _mutator_alloc_region 对象分配TLAB空间,在 _mutator_alloc_region 对象中,有个 _alloc_region 指针,该指针指向当前活跃的 region 空间,如果分配失败,则通过加锁的方式来分配对象。接下我们查看一下attempt_allocation 方法的实现

inline HeapWord* G1AllocRegion::attempt_allocation(size_t word_size,
                                                   bool bot_updates) {
  assert(bot_updates == _bot_updates, ar_ext_msg(this, "pre-condition"));

  //获得当前活跃的region
  HeapRegion* alloc_region = _alloc_region;
  assert(alloc_region != NULL, ar_ext_msg(this, "not initialized properly"));
  
  //分配对象内存
  HeapWord* result = par_allocate(alloc_region, word_size, bot_updates);
  if (result != NULL) {
    trace("alloc", word_size, result);
    return result;
  }
  trace("alloc failed", word_size);
  return NULL;
}

在该方法中,我们看到了 _alloc_region 指针,该指针执行当前活跃的 region,然后调用 par_allocate 方法,分配TLAB空间, par_allocate 最终回调用 par_allocate_impl 方法,分配TLAB空间。源码地址:hotspot\src\share\vm\memory\space.cpp

inline HeapWord* ContiguousSpace::par_allocate_impl(size_t size,
                                                    HeapWord* const end_value) {
  do {
    HeapWord* obj = top();
    if (pointer_delta(end_value, obj) >= size) {
      HeapWord* new_top = obj + size;
      //采用指针碰撞方式,分配对象内存(CAS)
      HeapWord* result = (HeapWord*)Atomic::cmpxchg_ptr(new_top, top_addr(), obj);
      // result can be one of two:
      //  the old top value: the exchange succeeded
      //  otherwise: the new value of the top is returned.
      if (result == obj) {
        assert(is_aligned(obj) && is_aligned(new_top), "checking alignment");
        return obj;
      }
    } else {
      return NULL;
    }
  } while (true);
}

如果通过 _mutator_alloc_region 对象分配TLAB空间失败,则会调用 attempt_allocation_slow 方法,通过该方法分配TLAB空间,源码如下:

HeapWord* G1CollectedHeap::attempt_allocation_slow(size_t word_size,
                                           unsigned int *gc_count_before_ret) {
  HeapWord* result = NULL;
  for (int try_count = 1; /* we'll return */; try_count += 1) {
    bool should_try_gc;
    unsigned int gc_count_before;
    {
      MutexLockerEx x(Heap_lock);//获取堆的锁

      result = _mutator_alloc_region.attempt_allocation_locked(word_size,
                                                      false /* bot_updates */);
      if (result != NULL) {
        return result;
      }
      //如果执行到这里,说明内存分配失败
      assert(_mutator_alloc_region.get() == NULL, "only way to get here");

      if (GC_locker::is_active_and_needs_gc()) {//JNI方法调用,执行扩容操作】
        //如果还有空闲的区域
        if (g1_policy()->can_expand_young_list()) {
          // No need for an ergo verbose message here,
          // can_expand_young_list() does this when it returns true.
          result = _mutator_alloc_region.attempt_allocation_force(word_size,
                                                      false /* bot_updates */);
          if (result != NULL) {
            return result;
          }
        }
        should_try_gc = false;
      } else {
        if (GC_locker::needs_gc()) {
          should_try_gc = false;
        } else {
          // Read the GC count while still holding the Heap_lock.
          gc_count_before = total_collections();
          should_try_gc = true;
        }
      }
    }

    if (should_try_gc) {
      bool succeeded;
      //触发GC
      result = do_collection_pause(word_size, gc_count_before, &succeeded);
      if (result != NULL) {
        assert(succeeded, "only way to get back a non-NULL result");
        return result;
      }

    ...............................中间省略部分代码..................................
    result = _mutator_alloc_region.attempt_allocation(word_size,
                                                      false /* bot_updates */);
    if (result != NULL) {
      return result;
    }
}

该方法篇幅较长,主要是先获得堆空间的锁,然后调用 attempt_allocation_locked 方法分配内存,如果分配失败,判断当前是否在执行JNI方法的调用,如果是执行扩容操作,如果不是则判断是否需要GC,如果 should_try_gc 为true,调用do_collection_pause 方法,触发一次GC操作。接下来,我们分析一下 attempt_allocation_locked 方法,源码如下:

inline HeapWord* G1AllocRegion::attempt_allocation_locked(size_t word_size,
                                                          bool bot_updates) {
  // First we have to tedo the allocation, assuming we're holding the
  // appropriate lock, in case another thread changed the region while
  // we were waiting to get the lock.
  HeapWord* result = attempt_allocation(word_size, bot_updates);
  if (result != NULL) {
    return result;
  }

  retire(true /* fill_up */);
  result = new_alloc_region_and_allocate(word_size, false /* force */);
  if (result != NULL) {
    trace("alloc locked (second attempt)", word_size, result);
    return result;
  }
  trace("alloc locked failed", word_size);
  return NULL;
}

从上面可以看出,会先通过调用 attempt_allocation 方法分配内存,由于上面已经分析过该方法,就不再次分析,如果调用 attempt_allocation 方法分配失败,则调用 new_alloc_region_and_allocate 方法,分配内存,具体实现如下,源码地址:\hotspot\src\share\vm\gc_implementation\g1\g1AllocRegion.cpp

HeapWord* G1AllocRegion::new_alloc_region_and_allocate(size_t word_size,
                                                       bool force) {
  assert(_alloc_region == _dummy_region, ar_ext_msg(this, "pre-condition"));
  assert(_used_bytes_before == 0, ar_ext_msg(this, "pre-condition"));
  trace("attempting region allocation");

  //分配一个新的region
  HeapRegion* new_alloc_region = allocate_new_region(word_size, force);
  if (new_alloc_region != NULL) {
    new_alloc_region->reset_pre_dummy_top();
    // Need to do this before the allocation
    _used_bytes_before = new_alloc_region->used();
    HeapWord* result = allocate(new_alloc_region, word_size, _bot_updates);
    assert(result != NULL, ar_ext_msg(this, "the allocation should succeeded"));

    OrderAccess::storestore();
    _alloc_region = new_alloc_region;
    _count += 1;
    trace("region allocation successful");
    return result;
  } else {
    trace("region allocation failed");
    return NULL;
  }
  ShouldNotReachHere();
}

该方法中调用了 allocate_new_region 方法分配新的 region,该方法最终调用 new_mutator_alloc_region 方法,分配region,具体实现如下:

HeapRegion* G1CollectedHeap::new_mutator_alloc_region(size_t word_size,
                                                      bool force) {
  assert_heap_locked_or_at_safepoint(true /* should_be_vm_thread */);
  assert(!force || g1_policy()->can_expand_young_list(),
         "if force is true we should be able to expand the young list");
  //判断当前young_list中的region数是否已经超过阈值
  bool young_list_full = g1_policy()->is_young_list_full();
  if (force || !young_list_full) {
    HeapRegion* new_alloc_region = new_region(word_size,
                                              false /* do_expand */);
    if (new_alloc_region != NULL) {
      set_region_short_lived_locked(new_alloc_region);
      _hr_printer.alloc(new_alloc_region, G1HRPrinter::Eden, young_list_full);
      return new_alloc_region;
    }
  }
  return NULL;
}

在 new_mutator_alloc_region 判断是否强强制分配内存,或者当前young_list中的region数是否已经超过阈值,如果判断成功,新分配一个region,如果失败,然后NULL;到此对象在TLAB空间如果分配的流程分析结束。

Eden空间分配

上一小节分析了在TLAB空间分配内存,如果在TLAB空间分配失败,则在Eden空间分配,在Eden空间分配主要是调用了下面方法:

 //在堆中分配
  result = Universe::heap()->mem_allocate(size,
                                          &gc_overhead_limit_was_exceeded);

在使用的G1垃圾收集器的情况下,内存被划分为多个region,将不同region划分给不同的年代,接下来分析在使用G1的情况下,对象如何分配,深入到 mem_allocate 方法中,源码地址:hotspot\src\share\vm\gc_implementation\g1\g1CollectedHeap.cpp

HeapWord*
G1CollectedHeap::mem_allocate(size_t word_size,
                              bool*  gc_overhead_limit_was_exceeded) {
  assert_heap_not_locked_and_not_at_safepoint();

  for (int try_count = 1; /* we'll return */; try_count += 1) {
    unsigned int gc_count_before;

    HeapWord* result = NULL;
    if (!isHumongous(word_size)) {
      //在Region里面分配对象
      result = attempt_allocation(word_size, &gc_count_before);
    } else {
     //在region中分配大对象
      result = attempt_allocation_humongous(word_size, &gc_count_before);
    }
    if (result != NULL) {
      return result;
    }

    // 触发GC
    VM_G1CollectForAllocation op(gc_count_before, word_size);
    // ...and get the VM thread to execute it.
    VMThread::execute(&op);
    ..........................省略.............................
}

在 mem_allocate 方法中,判断分配的对象是否是巨型对象,如果不是巨型对象,调用 attempt_allocation,该方法在上一小节已经分析,如果是巨型对象,调用attempt_allocation_humongous 方法,分配内存空间,如果分配失败,这触发一次GC,因为之前已经分析了 attempt_allocation 方法,该小节就不分析了,我们主要分析 attempt_allocation_humongous 方法,关键代码如下,源码地址:hotspot\src\share\vm\gc_implementation\g1\g1AllocRegion.cpp

{
      MutexLockerEx x(Heap_lock);

      //巨型对象不在年代代分配,先尝试分配
      result = humongous_obj_allocate(word_size);
      if (result != NULL) {
        return result;
      }

      if (GC_locker::is_active_and_needs_gc()) {
        should_try_gc = false;
      } else {
        //判断是否进行GC
        if (GC_locker::needs_gc()) {
          should_try_gc = false;
        } else {
          gc_count_before = total_collections();
          should_try_gc = true;
        }
      }
    }

    if (should_try_gc) {
      //触发GC
      bool succeeded;
      result = do_collection_pause(word_size, gc_count_before, &succeeded);
      if (result != NULL) {
        assert(succeeded, "only way to get back a non-NULL result");
        return result;
      }
   ....................省略..........................
}

在上面的方法中先尝试分配巨型对象,如果对象分配不成功,则判断是否触发GC,接下来我们分析 humongous_obj_allocate 方法的实现,关键代码如下:

  size_t word_size_rounded = round_to(word_size, HeapRegion::GrainWords);

 //需要的region数
  uint num_regions = (uint) (word_size_rounded / HeapRegion::GrainWords);
  uint x_num = expansion_regions();
  uint fs = _hrs.free_suffix();

  //从空闲可用的region列表中找到多个连续的region,并返回第一个region的序号
  uint first = humongous_obj_allocate_find_first(num_regions, word_size);
  if (first == G1_NULL_HRS_INDEX) {
    // The only thing we can do now is attempt expansion.
    if (fs + x_num >= num_regions) {
      assert(num_regions > fs, "earlier allocation should have succeeded");
      ergo_verbose1(ErgoHeapSizing,
                    "attempt heap expansion",
                    ergo_format_reason("humongous allocation request failed")
                    ergo_format_byte("allocation request"),
                    word_size * HeapWordSize);

      if (expand((num_regions - fs) * HeapRegion::GrainBytes)) {
        first = humongous_obj_allocate_find_first(num_regions, word_size);
      }
    }
  }

  HeapWord* result = NULL;
  if (first != G1_NULL_HRS_INDEX) {
    //分配内存
    result =
      humongous_obj_allocate_initialize_regions(first, num_regions, word_size);
    assert(result != NULL, "it should always return a valid result");
    //
    g1mm()->update_sizes();
  }

在该方法中,判断分配该巨型对象需要多少个region,并从空间的region列表中分配多个连续的region,返回第一个region的序号,接下来判断是否用那么多空闲的region,如果没有,进行扩容操作,接下来进行对象的分配。

参考链接

http://www.jianshu.com/p/a0efa489b99f

自我介绍

我是何勇,现在重庆猪八戒,多学学!!!

推荐阅读更多精彩内容