内存管理——Hotspot堆创建

堆创建

如何创建堆的过程虽然并不困难,但是涉及到的类比较多所有最好还是记录一下吧,万一忘了就麻烦了,只讨论使用Serial收集器的堆。

首先来列举一下关于堆管理的几个类好了:

  1. Heap
  2. Generation
  3. Space

Heap

Heap是关于堆的抽象,在JVM中只有CollectedHeap,继承了它的有下面几个类:

graph BT; GenCollectedHeap --> CollectedHeap SerialHeap --> GenCollectedHeap CMSHeap --> GenCollectedHeap G1CollectedHeap --> CollectedHeap ParallelScavengeHeap --> CollectedHeap ZCollectedHeap --> CollectedHeap

重点来看看GenCollectedHeap即可,SerialHeap拓展的内容并不多。Heap是堆管理中最为顶层的部分,堆中还会包含Generation,关于Generation会在后面进行介绍。

堆在Universe::initialize_heap()中被创建,删掉关于指针压缩后的内容非常简单:

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

  ThreadLocalAllocBuffer::set_max_size(Universe::heap()->max_tlab_size());

  // We will never reach the CATCH below since Exceptions::_throw will cause
  // the VM to exit if an exception is thrown during initialization

  if (UseTLAB) {
    assert(Universe::heap()->supports_tlab_allocation(),
           "Should support thread-local allocation buffers");
    ThreadLocalAllocBuffer::startup_initialization();
  }
  return JNI_OK;
}

一共做了2件事:

  1. 创建和初始化堆。
  2. 初始化TLAB配置。

我们只来看看关于如何创建和初始化堆就好了。

堆的创建

堆的创建会调用GCArguments::create_heap,不过这个是个纯虚函数,所以事实上会调用有关的重载,继承了GCArguments的类有很多,具体是调用了那个类的实现取决于当前的GC配置,如果是使用Serial收集器,那么会使用SerialArguments的实现:

// src/hotspot/share/gc/serial/serialArguments.cpp
CollectedHeap* SerialArguments::create_heap() {
  return create_heap_with_policy<SerialHeap, MarkSweepPolicy>();
}

// src/hotspot/share/gc/shared/gcArguments.inline.hpp
template <class Heap, class Policy>
CollectedHeap* GCArguments::create_heap_with_policy() {
  Policy* policy = new Policy();
  policy->initialize_all();
  return new Heap(policy);
}

可以看到,实际上会创建SerialHeap,创建了一个堆的配置策略MarkSweepPolicy,这个类是GenCollectorPolicy的子类。在对配置进行初始化的过程中会配置堆的最大大小、初始大小、最小大小和存活率等信息,配置的依据一个是虚拟机启动的参数,另外一个则是所谓的ergonomically,也就是自动配置。

再完成了对于配置策略的初始化之后,会使用它来创建堆,实际创建的堆是GenCollectedHeap

GenCollectedHeap::GenCollectedHeap(GenCollectorPolicy *policy,
                                   Generation::Name young,
                                   Generation::Name old,
                                   const char* policy_counters_name) :
  CollectedHeap(),
  _rem_set(NULL),
  _young_gen_spec(new GenerationSpec(young,
                                     policy->initial_young_size(),
                                     policy->max_young_size(),
                                     policy->gen_alignment())),
  _old_gen_spec(new GenerationSpec(old,
                                   policy->initial_old_size(),
                                   policy->max_old_size(),
                                   policy->gen_alignment())),
  _gen_policy(policy),
  _soft_ref_gen_policy(),
  _gc_policy_counters(new GCPolicyCounters(policy_counters_name, 2, 2)),
  _process_strong_tasks(new SubTasksDone(GCH_PS_NumElements)),
  _full_collections_completed(0) {
}

CollectedHeap中主要初始化了一些统计信息,所以跳过。可以看到这里将policy保存了下来,同时又创建了两个GenerationSpec对象,在对堆进行初始化的时候会使用到它们。同时,记忆集的字段_rem_set仍然是空的。

堆的初始化

堆的初始化这一块的逻辑和系统有比较多的关系,这里假设使用linux系统。

jint GenCollectedHeap::initialize() {
  // While there are no constraints in the GC code that HeapWordSize
  // be any particular value, there are multiple other areas in the
  // system which believe this to be true (e.g. oop->object_size in some
  // cases incorrectly returns the size in wordSize units rather than
  // HeapWordSize).
  guarantee(HeapWordSize == wordSize, "HeapWordSize must equal wordSize");

  // Allocate space for the heap.

  char* heap_address;
  ReservedSpace heap_rs;

  size_t heap_alignment = collector_policy()->heap_alignment();

  heap_address = allocate(heap_alignment, &heap_rs);

  if (!heap_rs.is_reserved()) {
    vm_shutdown_during_initialization(
      "Could not reserve enough space for object heap");
    return JNI_ENOMEM;
  }

  initialize_reserved_region((HeapWord*)heap_rs.base(), (HeapWord*)(heap_rs.base() + heap_rs.size()));

  _rem_set = create_rem_set(reserved_region());
  _rem_set->initialize();
  CardTableBarrierSet *bs = new CardTableBarrierSet(_rem_set);
  bs->initialize();
  BarrierSet::set_barrier_set(bs);

  ReservedSpace young_rs = heap_rs.first_part(_young_gen_spec->max_size(), false, false);
  _young_gen = _young_gen_spec->init(young_rs, rem_set());
  heap_rs = heap_rs.last_part(_young_gen_spec->max_size());

  ReservedSpace old_rs = heap_rs.first_part(_old_gen_spec->max_size(), false, false);
  _old_gen = _old_gen_spec->init(old_rs, rem_set());
  clear_incremental_collection_failed();

  return JNI_OK;
}

第一个调用的函数就是GenCollectedHeap::allocate,这个逻辑其实比较清晰但是调用链比较长,一直往下走就行了,总之最后会来到os::pd_reserve_memory

// src/hotspot/os/linux/os_linux.cpp
char* os::pd_reserve_memory(size_t bytes, char* requested_addr,
                            size_t alignment_hint) {
  return anon_mmap(requested_addr, bytes, (requested_addr != NULL));
}

此时的requested_addr参数为NULL。

anon_mmap则为:

// src/hotspot/os/linux/os_linux.cpp

// If 'fixed' is true, anon_mmap() will attempt to reserve anonymous memory
// at 'requested_addr'. If there are existing memory mappings at the same
// location, however, they will be overwritten. If 'fixed' is false,
// 'requested_addr' is only treated as a hint, the return value may or
// may not start from the requested address. Unlike Linux mmap(), this
// function returns NULL to indicate failure.
static char* anon_mmap(char* requested_addr, size_t bytes, bool fixed) {
  char * addr;
  int flags;

  flags = MAP_PRIVATE | MAP_NORESERVE | MAP_ANONYMOUS;
  if (fixed) {
    assert((uintptr_t)requested_addr % os::Linux::page_size() == 0, "unaligned address");
    flags |= MAP_FIXED;
  }

  // Map reserved/uncommitted pages PROT_NONE so we fail early if we
  // touch an uncommitted page. Otherwise, the read/write might
  // succeed if we have enough swap space to back the physical page.
  addr = (char*)::mmap(requested_addr, bytes, PROT_NONE,
                       flags, -1, 0); // 这样创建出来的内存并不能够被访问。

  return addr == MAP_FAILED ? NULL : addr;
}

传入的参数中requested_addr为NULL,bytes是新生代和老年代的最大大小相加的值,fixed为false。解释一下几个flag的意思:MAP_NORESERVE不要提前预留交换区空间、MAP_PRIVATEMAP_ANONYMOUS创建一个匿名的私有的虚拟映射,映射是其他进程不可见的。调用mmap的另外一个参数PROT_NONE的意思是禁止对映射的区域进行任何操作。

由于是写时复制的所以事实上并不会提前分配内存,但是可能会出现需要分配内存时系统已经没有足够的内存页和交换区了,不过这么做的好处是并不会真正占用内存。

返回到GenCollectedHeap::initialize

  char* heap_address;
  ReservedSpace heap_rs;

  size_t heap_alignment = collector_policy()->heap_alignment();

  heap_address = allocate(heap_alignment, &heap_rs);

可以发现,事实上allocate对heap_rs进行了初始化,这个对象就是用来表示一段内存的,来看看它的几个字段:

// ReservedSpace is a data structure for reserving a contiguous address range.

class ReservedSpace {
  friend class VMStructs;
 protected:
  char*  _base; // 基址。
  size_t _size; // 空间的长度。
  size_t _noaccess_prefix; // 是否有不可访问的前缀部分,如果开启指针压缩则会有,关于指针压缩机制可以参考[1]。
  size_t _alignment; // 对齐。
  bool   _special; // 是否是特殊的,貌似开启大页同时有一些其他限制就是特殊的。
  int    _fd_for_heap; // 文件描述符,如果映射到某个虚拟文件则会有(我不清楚是否可以映射其他类型的文件还是什么的),否则为-1。
 private:
  bool   _executable; // 是否可执行,在ReservedCodeSpace的构造函数中会初始化为true。
};

ReservedSpace的类继承结构如下:

graph BT; ReservedCodeSpace --> ReservedSpace ReservedHeapSpace --> ReservedSpace

对于ReservedCodeSpace的介绍会留到关于动态生成代码的内存管理中去。

继续回到GenCollectedHeap::initialize

  // 这一段并没有做什么,就是判断有没有初始化成功,以及使用`initialize_reserved_region`对几个字段进行了设置。
	if (!heap_rs.is_reserved()) {
    vm_shutdown_during_initialization(
      "Could not reserve enough space for object heap");
    return JNI_ENOMEM;
  }

  initialize_reserved_region((HeapWord*)heap_rs.base(), (HeapWord*)(heap_rs.base() + heap_rs.size()));
	
	// 创建了记忆集,同时创建了读写屏障,这部分的具体内容留到GC中。
  _rem_set = create_rem_set(reserved_region());
  _rem_set->initialize();
  CardTableBarrierSet *bs = new CardTableBarrierSet(_rem_set);
  bs->initialize();
  BarrierSet::set_barrier_set(bs);
	
	// 比较主要的部分。
  ReservedSpace young_rs = heap_rs.first_part(_young_gen_spec->max_size(), false, false);
  _young_gen = _young_gen_spec->init(young_rs, rem_set());
  heap_rs = heap_rs.last_part(_young_gen_spec->max_size());

  ReservedSpace old_rs = heap_rs.first_part(_old_gen_spec->max_size(), false, false);
  _old_gen = _old_gen_spec->init(old_rs, rem_set());
  clear_incremental_collection_failed(); // 和GC有关。

heap_rs.first_part的实现非常简单,就只是创建了一个新的Space,用来表示头_young_gen_spec->max_size个字节,然后用这个Space去初始化young_generation。

Generation

堆都是分代的,所以需要用Generation来表示代,比如在GenCollectedHeap中有两个字段_young_gen_old_gen分别用于表示young和old代。在更老的版本中(比如说openjdk-8),GenCollectedHeap允许使用更加多的代,但是到了目前(openjdk-11),只允许了两个代。这里只来看看和Serial收集器有关的代好了:

graph BT; DefNewGeneration --> Generation CardGeneration --> Generation TenuredGeneration --> CardGeneration

其中DefNewGenerationGenCollectedHeap中所使用的young代,而TenuredGeneration则是old代,对于什么样的heap要使用什么样的generation是提前配置好的。进行配置的有关调用在:

如果是的话CardGeneration还包含了记忆集和偏移表,分别保存在_rs_bts这两个字段,使用这两个数据结构来追踪老年代堆新生代中对象的引用。

GenCollectedHeap::initialize的代码中,我们实际上可以看到在_young_gen = _young_gen_spec->init(young_rs, rem_set());中使用了GenerationSpec,它是在构造GenCollectedHeap时创建的,其代码比较短:

// The specification of a generation.  This class also encapsulates
// some generation-specific behavior.  This is done here rather than as a
// virtual function of Generation because these methods are needed in
// initialization of the Generations.
class GenerationSpec : public CHeapObj<mtGC> {
  friend class VMStructs;
private:
  Generation::Name _name;
  size_t           _init_size;
  size_t           _max_size;

public:
  GenerationSpec(Generation::Name name, size_t init_size, size_t max_size, size_t alignment) :
    _name(name),
    _init_size(align_up(init_size, alignment)),
    _max_size(align_up(max_size, alignment))
  { }

  Generation* init(ReservedSpace rs, CardTableRS* remset);

  // Accessors
  Generation::Name name()        const { return _name; }
  size_t init_size()             const { return _init_size; }
  void set_init_size(size_t size)      { _init_size = size; }
  size_t max_size()              const { return _max_size; }
  void set_max_size(size_t size)       { _max_size = size; }
};

Space

Generation的内部还会被进一步划分为Space,比如在DefNewGeneration中就有_eden_space等三个Space。

posted @ 2022-09-21 23:00  aana  阅读(126)  评论(1编辑  收藏  举报