运行时数据区

2016/01/26 JVM

Java对象的内存分配

Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式、GC的实现方式、代的实现方式不同而具有不同的分配调用层次。 下面就以bytecodeInterpreter解释器对于new指令的解释出发,分析实例对象的内存分配过程:

 一、快速分配

1.实例的创建首先需要知道该类型是否被加载和正确解析,根据字节码所指定的CONSTANT_Class_info常量池索引,获取对象的类型信息并调用is_unresovled_klass()验证该类是否被解析过,在创建类的实例之前,必须确保该类型已经被正确加载和解析。

 CASE(_new): {
        u2 index = Bytes::get_Java_u2(pc+1);
        constantPoolOop constants = istate->method()->constants();
        if (!constants->tag_at(index).is_unresolved_klass()) {

2.接下来获取该类型在虚拟机中的表示instanceKlass(具体可以参考前文实例探索Java对象的组织结构) 

oop entry = constants->slot_at(index).get_oop();
          assert(entry->is_klass(), "Should be resolved klass");
          klassOop k_entry = (klassOop) entry;
          assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass");
          instanceKlass* ik = (instanceKlass*) k_entry->klass_part();

3.当类型已经被初始化并且可以被快速分配时,那么将根据UseTLAB来决定是否使用TLAB技术(Thread-Local Allocation Buffers,线程局部分配缓存技术)来将分配工作交由线程自行完成。TLAB是每个线程在Java堆中预先分配了一小块内存,当有对象创建请求内存分配时,就会在该块内存上进行分配,而不需要在Eden区通过同步控制进行内存分配。

复制代码

if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
            size_t obj_size = ik->size_helper();
            oop result = NULL;
            // If the TLAB isn't pre-zeroed then we'll have to do it
            bool need_zero = !ZeroTLAB;
            if (UseTLAB) {
              result = (oop) THREAD->tlab().allocate(obj_size);
            }
            if (result == NULL) {
              need_zero = true;

复制代码

4.如果不使用TLAB或在TLAB上分配失败,则会尝试在堆的Eden区上进行分配。Universe::heap()返回虚拟机内存体系所使用的CollectedHeap,其top_addr()返回的是Eden区空闲块的起始地址变量_top的地址,end_addr()是Eden区空闲块的结束地址变量_end的地址。故这里compare_to是Eden区空闲块的起始地址,new_top为使用该块空闲块进行分配后新的空闲块起始地址。这里使用CAS操作进行空闲块的同步操作,即观察_top的预期值,若与compare_to相同,即没有其他线程操作该变量,则将new_top赋给_top真正成为新的空闲块起始地址值,这种分配技术叫做bump-the-pointer(指针碰撞技术)。

  

复制代码

 retry:
              HeapWord* compare_to = *Universe::heap()->top_addr();
              HeapWord* new_top = compare_to + obj_size;
              if (new_top <= *Universe::heap()->end_addr()) {
                if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
                  goto retry;
                }
                result = (oop) compare_to;
              }
            }

复制代码

5.根据是否需要填0选项,对分配空间的对象数据区进行填0

复制代码

if (result != NULL) {
              // Initialize object (if nonzero size and need) and then the header
              if (need_zero ) {
                HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
                obj_size -= sizeof(oopDesc) / oopSize;
                if (obj_size > 0 ) {
                  memset(to_zero, 0, obj_size * HeapWordSize);
                }
              }

复制代码

6.根据是否使用偏向锁,设置对象头信息,然后设置对象的klassOop引用(这样对象本身就获取了获取类型数据的途径)

复制代码

if (UseBiasedLocking) {
                result->set_mark(ik->prototype_header());
              } else {
                result->set_mark(markOopDesc::prototype());
              }
              result->set_klass_gap(0);
              result->set_klass(k_entry);

复制代码

7.把对象地址引入栈,并继续执行下一个字节码

SET_STACK_OBJECT(result, 0);
              UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);

8.若该类型没有被解析,就会调用InterpreterRuntime的_new函数完成慢速分配

复制代码

// Slow case allocation
        CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
                handle_exception);
        SET_STACK_OBJECT(THREAD->vm_result(), 0);
        THREAD->set_vm_result(NULL);
        UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);

复制代码

以上就是快速分配的过程,其流程图如下,关键在于快速分配在Eden区所使用的无锁指针碰撞技术

img

 二、慢速分配

接下来看看慢速分配是如何进行的:   1.InterpreterRuntime的_new函数定义在/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp中:

复制代码

IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, constantPoolOopDesc* pool, int index))
  klassOop k_oop = pool->klass_at(index, CHECK);
  instanceKlassHandle klass (THREAD, k_oop);

  // Make sure we are not instantiating an abstract klass
  klass->check_valid_for_instantiation(true, CHECK);

  // Make sure klass is initialized
  klass->initialize(CHECK);

  oop obj = klass->allocate_instance(CHECK);
  thread->set_vm_result(obj);
IRT_END

复制代码

该函数在进行了对象类的检查(确保不是抽象类)和对该类型进行初始化后,调用instanceKlassHandle的allocate_instance进行内存分配。   其中instanceKlassHandle类由DEF_KLASS_HANDLE宏进行声明,注意该类重载了成员访问运算符”->”,这里的一系列成员方法的访问实际上是instanceKlass对象的访问。

 type*    operator -> () const       { return (type*)obj()->klass_part(); }

2.所以实际上是调用了instanceKlass的allocate_instance()成员函数:   allocate_instance()定义在/hotspot/src/share/vm/oops/instanceKlass.cpp   (1).检查是否设置了Finalizer函数,获取对象所需空间的大小

instanceOop instanceKlass::allocate_instance(TRAPS) {
     bool has_finalizer_flag = has_finalizer(); // Query before possible GC
     int size = size_helper();  // Query before forming handle.

(2).调用CollectedHeap的obj_allocate()创建一个instanceOop(堆上的对象实例),并根据情况注册Finalizer函数

复制代码

    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;

复制代码

3.CollectedHeap::ojb_allocate()定义在/hotspot/src/share/vm/gc_interface/CollectedHeap.hpp中,它将转而调用内联函数obj_allocate()

4.obj_allocate()定义在/hotspot/src/share/vm/gc_interface/CollectedHeap.inline.h中,若当正处于gc状态时,不允许进行内存分配申请,否则将调用common_mem_allocate_init()进行内存分配并返回获得内存的起始地址,随后将调用post_allocation_setup_obj()进行一些初始化工作 

复制代码

oop CollectedHeap::obj_allocate(KlassHandle klass, int size, TRAPS) {
 //...assert
  HeapWord* obj = common_mem_allocate_init(size, false, CHECK_NULL);
  post_allocation_setup_obj(klass, obj, size);
  NOT_PRODUCT(Universe::heap()->check_for_bad_heap_word_value(obj, size));
  return (oop)obj;
}

复制代码

5.common_mem_allocate_init()分为两部分,将分别调用common_mem_allocate_noinit()进行内存空间的分配和调用init_obj()进行对象空间的初始化

HeapWord* CollectedHeap::common_mem_allocate_init(size_t size, bool is_noref, TRAPS) {
  HeapWord* obj = common_mem_allocate_noinit(size, is_noref, CHECK_NULL);
  init_obj(obj, size);
  return obj;
}

6.common_mem_allocate_noinit()如下:   (1).若使用了本地线程分配缓冲TLAB,则会调用allocate_from_tlab()尝试从TLAB中分配内存

复制代码

  HeapWord* result = NULL;
  if (UseTLAB) {
    result = CollectedHeap::allocate_from_tlab(THREAD, size);
    if (result != NULL) {
      assert(!HAS_PENDING_EXCEPTION,
             "Unexpected exception, will result in uninitialized storage");
      return result;
    }
  }

复制代码

(2).否则会调用堆的mem_allocate()尝试分配

 

  bool gc_overhead_limit_was_exceeded = false;
  result = Universe::heap()->mem_allocate(size,
                                          is_noref,
                                          false,
                                          &gc_overhead_limit_was_exceeded);

(3).统计分配的字节数

 if (result != NULL) {
   //...
    THREAD->incr_allocated_bytes(size * HeapWordSize);
    return result;
  }

(4).否则说明申请失败,若在申请过程中gc没有超时,则抛出OOM异常

复制代码

if (!gc_overhead_limit_was_exceeded) {
    // -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support
    report_java_out_of_memory("Java heap space");

    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_JAVA_HEAP,
        "Java heap space");
    }

    THROW_OOP_0(Universe::out_of_memory_error_java_heap());

复制代码

7.对象内存分配后的初始化过程包括两部分,一个是init_obj()完成对对象内存空间的对齐和填充,一个是post_allocation_setup_obj()对堆上的oop对象进行初始化。

(1).init_obj():

复制代码

void CollectedHeap::init_obj(HeapWord* obj, size_t size) {
  assert(obj != NULL, "cannot initialize NULL object");
  const size_t hs = oopDesc::header_size();
  assert(size >= hs, "unexpected object size");
  ((oop)obj)->set_klass_gap(0);
  Copy::fill_to_aligned_words(obj + hs, size - hs);
}

复制代码

hs就是对象头的大小,fill_to_aligned_words将对象空间除去对象头的部分做填0处理,该函数定义在/hotspot/src/share/vm/utilities/copy.h中,并转而调用pd_fill_to_aligned_words()。   pd_fill_to_aligned_words根据不同平台实现,以x86平台为例,该函数定义在/hotspot/src/cpu/x86/vm/copy_x86.h中:

复制代码

static void pd_fill_to_words(HeapWord* tohw, size_t count, juint value) {
#ifdef AMD64
  julong* to = (julong*) tohw;
  julong  v  = ((julong) value << 32) | value;
  while (count-- > 0) {
    *to++ = v;
  }
#else
  juint* to = (juint*)tohw;
  count *= HeapWordSize / BytesPerInt;
  while (count-- > 0) {
    *to++ = value;
  }
#endif // AMD64
}

复制代码

该函数的作用就是先将地址类型转换,然后把堆的字数转化为字节数,再对该段内存进行填值(value = 0)处理

(2).post_allocation_setup_obj()调用了post_allocation_setup_common()进行初始化工作,然后调用post_allocation_notify()通知JVMTI和dtrace   

复制代码

void CollectedHeap::post_allocation_setup_obj(KlassHandle klass,
                                              HeapWord* obj,
                                              size_t size) {
  post_allocation_setup_common(klass, obj, size);
  assert(Universe::is_bootstrapping() ||
         !((oop)obj)->blueprint()->oop_is_array(), "must not be an array");
  // notify jvmti and dtrace
  post_allocation_notify(klass, (oop)obj);
}

复制代码

post_allocation_setup_common()如下:

复制代码

void CollectedHeap::post_allocation_setup_common(KlassHandle klass,
                                                 HeapWord* obj,
                                                 size_t size) {
  post_allocation_setup_no_klass_install(klass, obj, size);
  post_allocation_install_obj_klass(klass, oop(obj), (int) size);
}

复制代码

post_allocation_setup_no_klass_install()根据是否使用偏向锁,设置对象头信息等,即初始化oop的_mark字段。post_allocation_install_obj_klass()设置对象实例的klassOop引用,即初始化oop的_metadata(_klass/_compressed_klass)字段 。

以上内容就是堆实现无关的慢速分配过程,其流程图如下:

img

 三、堆的分配实现

1.mem_allocate将由堆的实现类型定义,以GenCollectedHeap为例:

复制代码

HeapWord* GenCollectedHeap::mem_allocate(size_t size,
                                         bool is_large_noref,
                                         bool is_tlab,
                                         bool* gc_overhead_limit_was_exceeded) {
  return collector_policy()->mem_allocate_work(size,
                                               is_tlab,
                                               gc_overhead_limit_was_exceeded);
}

复制代码

2.由之前分析,GenCollectedHeap根据用户配置有着不同的GC策略(默认的和配置UseSerialGC的MarkSweepPolicy、配置UseComcMarkSweepGC和UseAdaptiveSizePolicy的ASConcurrentMarkSweepPolicy、只配置UseComcMarkSweepGC的ConcurrentMarkSweepPolicy),但这里,对象内存空间的基本结构和分配的思想是一致的,所以统一由GenCollectorPolicy实现进行分代层级的对象分配操作,但具体的工作将交由各代的实现者来完成。

GenCollectedPolicy的mem_allocate_work()函数如下:   (1).gch指向GenCollectedHeap堆,内存分配请求将循环不断地进行尝试,直到分配成功或GC后分配失败

复制代码

HeapWord* GenCollectorPolicy::mem_allocate_work(size_t size,
                                        bool is_tlab,
                                        bool* gc_overhead_limit_was_exceeded) {
  GenCollectedHeap *gch = GenCollectedHeap::heap();
  //...
  // Loop until the allocation is satisified,
  // or unsatisfied after GC.
  for (int try_count = 1; /* return or throw */; try_count += 1) {  

复制代码

对于占用空间比较大的对象,如果经常放在新生代,那么剩余的内存空间就会非常紧张,将可能会导致新生代内存垃圾回收的频繁触发。故若对象的大小超过一定值,那么就不应该分配在新生代。

复制代码

   //...紧接上面部分
   HandleMark hm; // discard any handles allocated in each iteration

    // First allocation attempt is lock-free.
    Generation *gen0 = gch->get_gen(0);

    if (gen0->should_allocate(size, is_tlab)) {
      result = gen0->par_allocate(size, is_tlab);
      if (result != NULL) {
        assert(gch->is_in_reserved(result), "result not in heap");
        return result;
      }
    }

复制代码

若对象应该在新生代上分配,就会调用新生代的par_allocate()进行分配,注意在新生代普遍是采用复制收集器的,而内存的分配对应采用了无锁式的指针碰撞技术。

(2).在新生代上尝试无锁式的分配失败,那么就获取堆的互斥锁,并尝试在各代空间内进行内存分配

复制代码

unsigned int gc_count_before;  // read inside the Heap_lock locked region
    {
      MutexLocker ml(Heap_lock);
     //...
      bool first_only = ! should_try_older_generation_allocation(size);

      result = gch->attempt_allocation(size, is_tlab, first_only);
      if (result != NULL) {
        assert(gch->is_in_reserved(result), "result not in heap");
        return result;
      }

复制代码

其中should_try_older_generation_allocation()如下:

复制代码

bool GenCollectorPolicy::should_try_older_generation_allocation(
        size_t word_size) const {
  GenCollectedHeap* gch = GenCollectedHeap::heap();
  size_t gen0_capacity = gch->get_gen(0)->capacity_before_gc();
  return    (word_size > heap_word_size(gen0_capacity))
         || GC_locker::is_active_and_needs_gc()
         || gch->incremental_collection_failed();
}

复制代码

当进行gc前,新生代的空闲空间大小不足以分配对象,或者有线程触发了gc,或前一次的FullGC是由MinorGC触发的情况,都应该不再尝试再更高的内存代上进行分配,以保证新分配的对象尽可能在新生代空间上。

attempt_allocation()实现如下:

复制代码

HeapWord* GenCollectedHeap::attempt_allocation(size_t size,
                                               bool is_tlab,
                                               bool first_only) {
  HeapWord* res;
  for (int i = 0; i < _n_gens; i++) {
    if (_gens[i]->should_allocate(size, is_tlab)) {
      res = _gens[i]->allocate(size, is_tlab);
      if (res != NULL) return res;
      else if (first_only) break;
    }
  }
  // Otherwise...
  return NULL;
}

复制代码

即由低内存代向高内存代尝试分配内存

(3).从各个代空间都找不到可用的空闲内存(或不应该在更高的内存代上分配时),如果已经有线程触发了gc,那么当各代空间还有virtual space可扩展空间可用时,将会尝试扩展代空间并再次尝试进行内存分配,有点在gc前想尽一切办法获得内存的意思。

复制代码

if (GC_locker::is_active_and_needs_gc()) {
        if (is_tlab) {
          return NULL;  // Caller will retry allocating individual object
        }
        if (!gch->is_maximal_no_gc()) {
          // Try and expand heap to satisfy request
          result = expand_heap_and_allocate(size, is_tlab);
          // result could be null if we are out of space
          if (result != NULL) {
            return result;
          }
        }

复制代码

(4).否则各代已经没有可用的可扩展空间时,当当前线程没有位于jni的临界区时,将释放堆的互斥锁,以使得请求gc的线程可以进行gc操作,等待所有本地线程退出临界区和gc完成后,将继续循环尝试进行对象的内存分配

复制代码

JavaThread* jthr = JavaThread::current();
        if (!jthr->in_critical()) {
          MutexUnlocker mul(Heap_lock);
          // Wait for JNI critical section to be exited
          GC_locker::stall_until_clear();
          continue;
        }

复制代码

(5).若各代无法分配对象的内存,并且没有gc被触发,那么当前请求内存分配的线程将发起一次gc,这里将提交给VM一个GenCollectForAllocation操作以触发gc,当操作执行成功并返回时,若gc锁已被获得,那么说明已经由其他线程触发了gc,将继续循环以等待gc完成

复制代码

VM_GenCollectForAllocation op(size,
                                  is_tlab,
                                  gc_count_before);
    VMThread::execute(&op);
    if (op.prologue_succeeded()) {
      result = op.result();
      if (op.gc_locked()) {
         assert(result == NULL, "must be NULL if gc_locked() is true");
         continue;  // retry and/or stall as necessary
      }

复制代码

否则将等待gc完成,若gc超时则会将gc_overhead_limit_was_exceeded设置为true返回给调用者,并重置超时状态,并对分配的对象进行填充处理

复制代码

    const bool limit_exceeded = size_policy()->gc_overhead_limit_exceeded();
      const bool softrefs_clear = all_soft_refs_clear();
      assert(!limit_exceeded || softrefs_clear, "Should have been cleared");
      if (limit_exceeded && softrefs_clear) {
        *gc_overhead_limit_was_exceeded = true;
        size_policy()->set_gc_overhead_limit_exceeded(false);
        if (op.result() != NULL) {
          CollectedHeap::fill_with_object(op.result(), size);
        }
        return NULL;
      }

复制代码

以上内容就是堆的实现相关、但代/GC实现无关的分配过程,其流程图归纳如下:

img

分类: Hotspot源码探索

源码分析:Java堆的创建

虚拟机在内存中申请一片区域,由虚拟机自动管理,用来满足应用程序对象分配的空间需求,即堆空间。

由于程序运行的局部特性,程序创建的大多数对象都具有非常短的生命周期,而程序也会创建一些生命周期特别长的对象。简单的复制收集器无论对象的生命周期是长是短,都会进行复制操作。而生命周期较长的对象在多次垃圾回收期间内并不会被回收,这就使得这些对象被来回复制而使得算法性能大大下降。

分代收集把堆分为多个子堆,分别用来存放不同寿命的对象。新生对象空间的将经历最频繁的垃圾回收,而对于经历了若干次垃圾收集后仍然存活的对象,将成长为成熟对象,并移动到成熟对象的子堆中,而对老生代子堆的垃圾回收就不会像新生对象子堆那么频繁。

HotSpot的堆空间分为新生代(YoungGen)和老年代(OldGen,此外还有位于非堆空间的永久代,但在Java8中将移除永久代),新生代又分为Eden区和2个Survivor区(From/To)用以进行复制收集垃圾对象。 对Java堆和对象的分析将从Java堆的创建开始,然后分析Java对象的分配与垃圾回收。

 一、堆的实现方式

在虚拟机的创建初始化过程中,通过调用Universe的成员函数initialize_heap()将完成Java堆的初始化。在Universe模块下的初始化将根据虚拟机选项来选择堆的具体实现方式:   1.若虚拟机配置UseParallelGC,则Java堆的堆类型为ParallelScavengeHeap(并行收集堆)

复制代码

//定义在/hotspot/src/share/vm/memory/universe.cpp中
if (UseParallelGC) {
#ifndef SERIALGC
    Universe::_collectedHeap = new ParallelScavengeHeap();
#else  // SERIALGC
    fatal("UseParallelGC not supported in java kernel vm.");
#endif // SERIALGC

  }

复制代码

2.若虚拟机配置UseG1GC,那么将选择堆类型为G1CollectedHeap,垃圾收集策略将使用专用的G1CollectorPolicy(垃圾优先收集)策略

复制代码

 else if (UseG1GC) {
#ifndef SERIALGC
    G1CollectorPolicy* g1p = new G1CollectorPolicy_BestRegionsFirst();
    G1CollectedHeap* g1h = new G1CollectedHeap(g1p);
    Universe::_collectedHeap = g1h;
#else  // SERIALGC
    fatal("UseG1GC not supported in java kernel vm.");
#endif // SERIALGC

  }

复制代码

3.否则,虚拟机将使用GenCollectedHeap(分代收集堆)

Universe::_collectedHeap = new GenCollectedHeap(gc_policy);

各个堆实现类的类关系如下:

img

对于默认情况下的堆实现,还要根据配置选择垃圾回收策略gc_policy来构造一个GenCollectedHeap,这里根据虚拟机配置选择不同的GC策略:   (1).若虚拟机配置UseSerialGC,那么将使用MarkSweepPolicy(标记-清除)策略

GenCollectorPolicy *gc_policy;

    if (UseSerialGC) {
      gc_policy = new MarkSweepPolicy();
    }

(2).若虚拟机配置UseConcMarkSweepGC和UseAdaptiveSizePolicy,那么将使用ASConcurrentMarkSweepPolicy(自适应并发标记-清除)策略,若没有指定UseAdaptiveSizePolicy,虚拟机将默认使用ConcurrentMarkSweepPolicy(并发标记-清除)策略

复制代码

else if (UseConcMarkSweepGC) {
#ifndef SERIALGC
      if (UseAdaptiveSizePolicy) {
        gc_policy = new ASConcurrentMarkSweepPolicy();
      } else {
        gc_policy = new ConcurrentMarkSweepPolicy();
      }

复制代码

(3).若没有进行配置,虚拟机将默认使用MarkSweepPolicy策略

else { // default old generation
      gc_policy = new MarkSweepPolicy();
    }

如下表所示:

img

其中垃圾回收策略类的关系如下图:

img

4.接下来是相应实现的堆的初始化

jint status = Universe::heap()->initialize();
  if (status != JNI_OK) {
    return status;
  }

5.堆空间初始化完成后,是LP64平台上的指针压缩以及TLAB的相关内容 。   通常64位JVM消耗的内存会比32位的大1.5倍,这是因为在64位环境下,对象将使用64位指针,这就增加了一倍的指针占用内存开销。从JDK 1.6 update14开始,64 bit JVM正式支持了 -XX:+UseCompressedOops 选项来压缩指针,以节省内存空间。 指针压缩的地址计算如下:

addr = <narrow_oop_base> + <narrow_oop> << 3 + <field_offset>

若堆寻址空间小于4GB(2^32)时,直接使用32位的压缩对象指针< narrow_oop >就可以找到该对象   若堆寻址空间大于4GB(2^32)但小于32GB时,就必须借助偏移来获得真正的地址(对象是8字节对齐的)。   若堆寻址空间大于32GB时,就需要借助堆的基址来完成寻址了,< narrow_oop_base >为堆的基址,< field_offset >为一页的大小。   (1).若heap的地址空间的最大地址大于OopEncodingHeapMax(32GB),则设置基础地址为当前堆的起始地址-页大小,设置偏移为LogMinObjAlignmentInBytes(3),即使用普通的对象指针压缩技术

if ((uint64_t)Universe::heap()->reserved_region().end() > OopEncodingHeapMax) {
      // Can't reserve heap below 32Gb.
      Universe::set_narrow_oop_base(Universe::heap()->base() - os::vm_page_size());
      Universe::set_narrow_oop_shift(LogMinObjAlignmentInBytes);
}

(2).否则设置基础地址为0

else {
      Universe::set_narrow_oop_base(0);
      //...
      }

若heap的地址空间的最大地址大于NarrowOopHeapMax(4GB,小于32GB),则设置偏移为LogMinObjAlignmentInBytes(默认为3),即使用零基压缩技术,否则设置偏移为0,即直接使用压缩对象指针进行寻址

if((uint64_t)Universe::heap()->reserved_region().end() > NarrowOopHeapMax) {
        // Can't reserve heap below 4Gb.
        Universe::set_narrow_oop_shift(LogMinObjAlignmentInBytes);
      } else {
        Universe::set_narrow_oop_shift(0);

 二、堆的初始化:分代实现方式

接下来分析特定堆的初始化过程,这里以GenCollectedHeap和MarkSweepPolicy为例:  

  GenCollectedHeap的构造函数中使用传入的策略作为_gen_policy(代策略)。以MarkSweepPolicy为例,看看其构造函数:

//定义在/hotspot/src/share/vm/memory/collectorPolicy.cpp中
MarkSweepPolicy::MarkSweepPolicy() {
  initialize_all();
}

MarkSweepPolicy的构造函数调用了initialize_all()来完成策略的初始化,initialize_all()是父类GenCollectorPolicy()的虚函数,它调用了三个子初始化虚函数,这三个子初始化过程由GenCollectorPolicy的子类实现。其中initialize_flags()初始化了永久代的一些大小配置参数,initialize_size_info()设置了Java堆大小的相关参数,initialize_generations()根据用户参数,配置各内存代的管理器。

复制代码

//hotspot/src/share/vm/memory/collectorPolicy.hpp中
virtual void initialize_all() {
    initialize_flags();
    initialize_size_info();
    initialize_generations();
  }

复制代码

下面通过initialize_generations()来看看各代有哪些实现方式:   1.若配置了UseParNewGC,并且并行GC线程数大于1,那么新生代就会使用ParNew实现

复制代码

//永久代初始化
  _generations = new GenerationSpecPtr[number_of_generations()];
  //...

  if (UseParNewGC && ParallelGCThreads > 0) {
    _generations[0] = new GenerationSpec(Generation::ParNew, _initial_gen0_size, _max_gen0_size);
  }

复制代码

2.默认新生代使用DefNew实现

else {
    _generations[0] = new GenerationSpec(Generation::DefNew, _initial_gen0_size, _max_gen0_size);
  }

3.老年代固定使用MarkSweepCompact实现

_generations[1] = new GenerationSpec(Generation::MarkSweepCompact, _initial_gen1_size, _max_gen1_size);

(其中DefNew、ParNew、MarkSweepCompact等均为Generation的枚举集合Name的成员,描述了可能实现的各种代实现类型) MarkSweepPolicy、ConcurrentMarkSweepPolicy、ASConcurrentMarkSweepPolicy对各代的实现综合如下表所示:

img

 三、堆的初始化:堆内存空间分配

分析完了构造函数,回到Universe模块中堆的initialize()。   以GenCollectedHeap为例:   1.根据构造函数传入的gc_policy(分代策略)来初始化分代数

//定义在/hotspot/src/share/vm/memory/genCollectedHeap.cpp中
jint GenCollectedHeap::initialize() {
  //...
  _n_gens = gen_policy()->number_of_generations();

根据GenCollectedHeap的定义可以看到,GenCollectedHeap最多支持10个分代

复制代码

 enum SomeConstants {
    max_gens = 10
  };

//...
 private:
  int _n_gens;
  Generation* _gens[max_gens];

复制代码

其实并不需要这么多分代,MarkSweepPolicy、ConcurrentMarkSweepPolicy、ASConcurrentMarkSweepPolicy(ConcurrentMarkSweepPolicy的子类)均有着共同的祖先类TwoGenerationCollectorPolicy,其分代只有2代,即新生代和老年代。

2.每代的大小是基于GenGrain大小对齐的

 // The heap must be at least as aligned as generations.
  size_t alignment = Generation::GenGrain;

GenGrain定义在/hotspot/src/share/vm/memory/generation.h中,在非ARM平台中是2^16字节,即64KB大小   3.获取各分代的管理器指针数组和永久代的管理器指针,并对齐各代的大小到64KB

复制代码

PermanentGenerationSpec *perm_gen_spec =
                                collector_policy()->permanent_generation();

  // Make sure the sizes are all aligned.
  for (i = 0; i < _n_gens; i++) {
    _gen_specs[i]->align(alignment);
  }
  perm_gen_spec->align(alignment);

复制代码

GenerationSpec的align()定义在/hotspot/src/share/vm/memory/generationSpec.h,使初始和最大大小值向上对齐至64KB的倍数

// Alignment
  void align(size_t alignment) {
    set_init_size(align_size_up(init_size(), alignment));
    set_max_size(align_size_up(max_size(), alignment));
  }

4.调用allocate()为堆分配空间,其起始地址为heap_address

复制代码

char* heap_address;
  size_t total_reserved = 0;
  int n_covered_regions = 0;
  ReservedSpace heap_rs(0);

  heap_address = allocate(alignment, perm_gen_spec, &total_reserved,
                          &n_covered_regions, &heap_rs);

复制代码

5.初始分配所得的空间将被封装在_reserved(CollectedHeap的MemRegion成员)中

_reserved = MemRegion((HeapWord*)heap_rs.base(),
                        (HeapWord*)(heap_rs.base() + heap_rs.size()));

调整实际的堆大小为去掉永久代的misc_data和misc_code的空间,并创建一个覆盖整个空间的数组,数组每个字节对应于堆的512字节,用于遍历新生代和老年代空间

复制代码

  _reserved.set_word_size(0);
  _reserved.set_start((HeapWord*)heap_rs.base());
  size_t actual_heap_size = heap_rs.size() - perm_gen_spec->misc_data_size()
                                           - perm_gen_spec->misc_code_size();
  _reserved.set_end((HeapWord*)(heap_rs.base() + actual_heap_size));

  _rem_set = collector_policy()->create_rem_set(_reserved, n_covered_regions);
  set_barrier_set(rem_set()->bs());

复制代码

7.调用heap_rs的的first_part(),依次为新生代和老年代分配空间并调用各代管理器的init()将其初始化为Generation空间,最后为永久代分配空间和进行初始化

复制代码

_gch = this;

  for (i = 0; i < _n_gens; i++) {
    ReservedSpace this_rs = heap_rs.first_part(_gen_specs[i]->max_size(),
                                              UseSharedSpaces, UseSharedSpaces);
    _gens[i] = _gen_specs[i]->init(this_rs, i, rem_set());
    heap_rs = heap_rs.last_part(_gen_specs[i]->max_size());
  }
  _perm_gen = perm_gen_spec->init(heap_rs, PermSize, rem_set());

复制代码

  四、内存空间申请实现

那么GenCollectedHeap是如何向系统申请内存空间的呢?   答案就在allocate()函数中   1.在申请之前,当然要对内存空间的大小和分块数进行计算   (1).内存页的大小将根据虚拟机是否配置UseLargePages而不同,large_page_size在不同平台上表现不同,x86使用2/4M(物理地址扩展模式)的页大小,AMD64使用2M,否则,Linux默认内存页大小只有4KB,接下来会以各代所配置的最大大小进行计算,若最大值设置为负数,那么jvm将报错退出,默认的新生代和老年代的分块数为1,而永久代的分块数为2

复制代码

char* GenCollectedHeap::allocate(size_t alignment,
                                 PermanentGenerationSpec* perm_gen_spec,
                                 size_t* _total_reserved,
                                 int* _n_covered_regions,
                                 ReservedSpace* heap_rs){
  //...
  // Now figure out the total size.
  size_t total_reserved = 0;
  int n_covered_regions = 0;
  const size_t pageSize = UseLargePages ?
      os::large_page_size() : os::vm_page_size();

  for (int i = 0; i < _n_gens; i++) {
    total_reserved += _gen_specs[i]->max_size();
    if (total_reserved < _gen_specs[i]->max_size()) {
      vm_exit_during_initialization(overflow_msg);
    }
    n_covered_regions += _gen_specs[i]->n_covered_regions();
  }

复制代码

加上永久代空间的大小和块数

total_reserved += perm_gen_spec->max_size();
if (total_reserved < perm_gen_spec->max_size()) {
    vm_exit_during_initialization(overflow_msg);
  }
  n_covered_regions += perm_gen_spec->n_covered_regions();

(2).加上永久代的misc_data和misc_code的空间大小(数据区和代码区),但其实并不是堆的一部分

size_t s = perm_gen_spec->misc_data_size() + perm_gen_spec->misc_code_size();

  total_reserved += s;

(3).如果配置了UseLargePages,那么将向上将申请的内存空间大小对齐至页

复制代码

if (UseLargePages) {
    assert(total_reserved != 0, "total_reserved cannot be 0");
    total_reserved = round_to(total_reserved, os::large_page_size());
    if (total_reserved < os::large_page_size()) {
      vm_exit_during_initialization(overflow_msg);
    }
  }

复制代码

(4).对象地址压缩的内容 根据UnscaledNarrowOop(直接使用压缩指针)选取合适的堆起始地址,并尝试在该地址上分配内存

复制代码

 if (UseCompressedOops) {
      heap_address = Universe::preferred_heap_base(total_reserved, Universe::UnscaledNarrowOop);
      *_total_reserved = total_reserved;
      *_n_covered_regions = n_covered_regions;
      *heap_rs = ReservedHeapSpace(total_reserved, alignment,
                                   UseLargePages, heap_address);

复制代码

若不能再该地址进行分配内存,则尝试使用ZereBasedNarrowOop(零基压缩)尝试在更高的地址空间上进行分配

复制代码

if (heap_address != NULL && !heap_rs->is_reserved()) {
        // Failed to reserve at specified address - the requested memory
        // region is taken already, for example, by 'java' launcher.
        // Try again to reserver heap higher.
        heap_address = Universe::preferred_heap_base(total_reserved, Universe::ZeroBasedNarrowOop);
        *heap_rs = ReservedHeapSpace(total_reserved, alignment,
                                     UseLargePages, heap_address);

复制代码

若仍然失败,则使用普通的指针压缩技术在其他地址上进行分配

复制代码

 if (heap_address != NULL && !heap_rs->is_reserved()) {
          // Failed to reserve at specified address again - give up.
          heap_address = Universe::preferred_heap_base(total_reserved, Universe::HeapBasedNarrowOop);
          assert(heap_address == NULL, "");
          *heap_rs = ReservedHeapSpace(total_reserved, alignment,
                                       UseLargePages, heap_address);
        }
      }

复制代码

2.调用ReservedHeapSpace的构造函数进行内存空间的申请

复制代码

  *_total_reserved = total_reserved;
  *_n_covered_regions = n_covered_regions;
  *heap_rs = ReservedHeapSpace(total_reserved, alignment,
                               UseLargePages, heap_address);

  return heap_address;

复制代码

在构造函数中并没有发现对内存空间进行申请,那么继续看父类ReservedSpace的构造函数

复制代码

ReservedSpace::ReservedSpace(size_t size, size_t alignment,  
                             bool large,  
                             char* requested_address,  
                             const size_t noaccess_prefix) {  
  initialize(size+noaccess_prefix, alignment, large, requested_address,  noaccess_prefix, false);  
} 

复制代码

3.initialize()的实现如下:   (1).如果目标操作系统不支持large_page_memory,那么将进行特殊处理,此外,对指针压缩处理还需要对请求分配的内存空间大小进行调整

if (requested_address != 0) {  
    requested_address -= noaccess_prefix; // adjust requested address  
    assert(requested_address != NULL, "huge noaccess prefix?");  
  } 

(2).对于上述特殊情况,会调用reserve_memory_special()进行内存空间的申请,并若申请成功会进行空间大小的对齐验证

复制代码

if (special) {  

    //向操作系统申请指定大小的内存,并映射到用户指定的内存空间中  
    base = os::reserve_memory_special(size, requested_address, executable);  

    if (base != NULL) {  
      if (failed_to_reserve_as_requested(base, requested_address, size, true)) {  
        // OS ignored requested address. Try different address.  
        return;  
      }  
      // Check alignment constraints  
      assert((uintptr_t) base % alignment == 0, "Large pages returned a non-aligned address");  
      _special = true; 

复制代码

(3).若配置了UseSharedSpace或UseCompressedOops,那么堆将在指定地址进行申请,就会调用attempt_reserve_memory_at()进行申请,否则,调用reserve_memory()进行申请

复制代码

if (requested_address != 0) {  
      base = os::attempt_reserve_memory_at(size, requested_address);  

      if (failed_to_reserve_as_requested(base, requested_address, size, false)) {  
        // OS ignored requested address. Try different address.  
        base = NULL;  
      }  
    } else {  
      base = os::reserve_memory(size, NULL, alignment);  
    }  

复制代码

(4).若分配成功,还需要对分配的起始地址进行对齐验证。若没有对齐,则会进行手工调整。调整的方法为尝试申请一块size+alignment大小的空间,若成功则向上对齐所得的内存空间的起始地址(失败则无法对齐,直接返回),并以此为起始地址重新申请一块size大小的空间,这块size大小的空间必然包含于size+alignment大小的空间内,以此达到对齐地址的目的。

复制代码

// Check alignment constraints  
    if ((((size_t)base + noaccess_prefix) & (alignment - 1)) != 0) {  
      // Base not aligned, retry  
      if (!os::release_memory(base, size)) fatal("os::release_memory failed");  
      // Reserve size large enough to do manual alignment and  
      // increase size to a multiple of the desired alignment  
      size = align_size_up(size, alignment);  
      size_t extra_size = size + alignment;  
      do {  
        char* extra_base = os::reserve_memory(extra_size, NULL, alignment);  
        if (extra_base == NULL) return;  

        // Do manual alignement  
        base = (char*) align_size_up((uintptr_t) extra_base, alignment);  
        assert(base >= extra_base, "just checking");  
        // Re-reserve the region at the aligned base address.  
        os::release_memory(extra_base, extra_size);  
        base = os::reserve_memory(size, base);  
      } while (base == NULL); 

复制代码

最后,在地址空间均已分配完毕,GenCollectedHeap的initialize()中为各代划分了各自的内存空间范围,就会调用各代的GenerationSpec的init()函数完成各代的初始化。

复制代码

switch (name()) {
    case PermGen::MarkSweepCompact:
      return new CompactingPermGen(perm_rs, shared_rs, init_size, remset, this);

#ifndef SERIALGC
    case PermGen::MarkSweep:
      guarantee(false, "NYI");
      return NULL;

    case PermGen::ConcurrentMarkSweep: {
      assert(UseConcMarkSweepGC, "UseConcMarkSweepGC should be set");
      CardTableRS* ctrs = remset->as_CardTableRS();
      if (ctrs == NULL) {
        vm_exit_during_initialization("RemSet/generation incompatibility.");
      }
      // XXXPERM
      return new CMSPermGen(perm_rs, init_size, ctrs,
                   (FreeBlockDictionary::DictionaryChoice)CMSDictionaryChoice);
    }
#endif // SERIALGC
    default:
      guarantee(false, "unrecognized GenerationName");
      return NULL;
  }

复制代码

各分代实现类的类关系如下:

img

归纳堆初始化的流程图如下:

img

Search

    微信好友

    博士的沙漏

    Table of Contents