ZGC源码分析(2)- ZGC内存管理

ZGC为了支持TB级内存,设计了分页管理(类似于G1的分区);为了能够快速的进行并发标记和并发移动,对内存空间重新进行了划分,这就是Color Pointers,同时设计了物理内存和虚拟内存两级内存管理。注意:这里的虚拟内存和操作系统的虚拟内存概念是一样的;但是物理内存和操作系统中的物理内存并不一样,ZGC是借用了物理地址这个概念;但是ZGC中物理地址和操作系统的物理地址是映射关联的,所以为了清晰的了解ZGC内存管理,我们有必要先了解一下操作系统的虚拟内存和物理内存。

操作系统地址管理

物理内存非常直观,就是真实存在的,插在主板内存槽上的内存条的容量的大小。我们经常所说的一台计算机的配置有1GB或者2GB内存,这个就是真实的物理内存。而虚拟内存是伴随着操作系统和硬件的发展出现的。
虚拟地址是操作系统根据CPU的寻址能力,支持访问的虚拟空间,比如前些年大家使用的32位系统,对应的虚拟地址空间为0-2^32,即0-4G,而我们的计算机的物理内存可能只有512MB。虚拟内存的发展解决了很多问题,也带了很多好处。具体可以参考《深入理解计算机系统结构》
当程序试图访问一个虚存页面时,这个请求会通过操作系统来访问真正的内存,首先到页表中去查询该页是否已映射到物理页框中,并记录在页表中。如果在,则会通过MMU把页码转换成页框码,并加上虚拟地址提供的页内偏移量形成物理地址后去访问物理内存;如果不在,则意味着该虚存页面还没有被载入内存,这时MMU就会通知操作系统:发生了一个页面访问错误(页面错误),接下来系统会启动所谓的“请页”机制,即调用相应的系统操作函数,判断该虚拟地址是否为有效地址。如果是有效的地址,就从虚拟内存中将该地址指向的页面读入到内存中的一个空闲页框中,并在页表中添加上相对应的表项,最后处理器将从发生页面错误的地方重新开始运行;如果是无效的地址,则表明进程在试图访问一个不存在的虚拟地址,此时操作系统将终止此次访问。当然,也存在这样的情况:在请页成功之后,内存中已没有空闲物理页框了。这是,系统必须启动所谓地“交换”机制,即调用相应的内核操作函数,在物理页框中寻找一个当前不再使用或者近期可能不会用到的页面所占据的页框。找到后,就把其中的页移出,以装载新的页面。对移出页面根据两种情况来处理:如果该页未被修改过,则删除它;如果该页曾经被修改过,则系统必须将该页写回辅存。

关于虚拟地址更多的内容可以参考其他的网页:比例https://www.cnblogs.com/shijingjing07/p/5611579.html

地址空间设计

ZGC目前林支持64位Linux,最多管理4TB的内存。不知道你有没有注意到这个地方似乎有点问题,64位系统支持的内存远超过4TB,那么为什么我们一直强调它只能支持4TB,为什么不使用更多的虚拟内存?ZGC对整个内存空间进行划分,这是来自于源码中关于地址空间的一个说明,如下:

image.png

简单来说,整个地址空间被分成3个视图,分别是:Mark0,Mark1和Remapped。而且有意思的是这3个视图会映射到操作系统的同一物理地址。这里就涉及到ZGC中Color Pointers的概念。
我们先来看一下地址空间的设计,如何映射的,再分析一下为什么这么做以及这么做的优点。
ZGC支持64位系统,其中42位用于地址,4位用于描述元数据,其实就是大家所说的Color Pointers,还有1位目前暂时没有使用,最高17位固定为0。具体如下图:

image.png

由于42位地址最大的寻址空间就是4TB,这就是为什么ZGC一直宣称自己最大支持4TB内存的原因。这里还有视图的概念,Mark0、Mark1和Remapped就是3个视图,分别用第43、44、45位上设置1,就是对应的视图。所以这里的意思就是这4位标记并不是用于地址寻址使用的。他们所使用的地址都是最低42位所对应的地址。视图是什么意思呢?简单的回答就是这3个虚拟地址映射到一个物理地址,而为了区分这三个虚拟地址,称他们为视图。下面我们看一下ZGC是如何进行抵制映射创建视图。

ZGC虚拟地址映射

熟悉Linux编程的人可以略过这一部分。把多个虚拟地址映射到一个物理地址的步骤可以总结如下:

  • 打开一个文件描述,这个文件描述符可以是内存文件描述符也可以是普通文件描述符(最好是内存文件描述符)
  • 把多个地址使用mmap映射到这个文件描述符上

先看下如何创建文件描述,具体代码

jdk11u/src/hotspot/os_cpu/linux_x86/gc/z/zBackingFile_linux_x86.cpp

这个代码有一个create_fd的函数就是创建文件描述符,优先创建内存文件描述符,如果不能成功则创建一个磁盘文件描述符。

int ZBackingFile::create_fd(const char* name) const {
  if (ZPath == NULL) {
    // If the path is not explicitly specified, then we first try to create a memfd file
    // instead of looking for a tmpfd/hugetlbfs mount point. Note that memfd_create() might
    // not be supported at all (requires kernel >= 3.17), or it might not support large
    // pages (requires kernel >= 4.14). If memfd_create() fails, then we try to create a
    // file on an accessible tmpfs or hugetlbfs mount point.
    const int fd = create_mem_fd(name);
    if (fd != -1) {
      return fd;
    }

    log_debug(gc, init)("Falling back to searching for an accessible mount point");
  }

  return create_file_fd(name);
}

创建内存文件描述符通过系统调用memfd_create完成,由于memfd_create是内核态才能调用的函数,所以必须通过syscall函数从用户态进入内核态,并传递参数__NR_memfd_create,最终操作系统调用相应的函数完成。

int ZBackingFile::create_mem_fd(const char* name) const {
  // Create file name
  char filename[PATH_MAX];
  snprintf(filename, sizeof(filename), "%s%s", name, ZLargePages::is_explicit() ? ".hugetlb" : "");

  // Create file
  const int extra_flags = ZLargePages::is_explicit() ? MFD_HUGETLB : 0;
  const int fd = z_memfd_create(filename, MFD_CLOEXEC | extra_flags);
  if (fd == -1) {
    ZErrno err;
    log_debug(gc, init)("Failed to create memfd file (%s)",
                        ((UseLargePages && err == EINVAL) ? "Hugepages not supported" : err.to_string()));
    return -1;
  }

  log_info(gc, init)("Heap backed by file: /memfd:%s", filename);

  return fd;
}
static int z_memfd_create(const char *name, unsigned int flags) {
  return syscall(__NR_memfd_create, name, flags);
}

创建文件描述,这里要注意的是ZGC支持大页面,所以会根据具体的文件系统信息创建不同的文件描述符。

int ZBackingFile::create_file_fd(const char* name) const {
  const char* const filesystem = ZLargePages::is_explicit()
                                 ? ZFILESYSTEM_HUGETLBFS
                                 : ZFILESYSTEM_TMPFS;
  const char** const preferred_mountpoints = ZLargePages::is_explicit()
                                             ? z_preferred_hugetlbfs_mountpoints
                                             : z_preferred_tmpfs_mountpoints;

  // Find mountpoint
  ZBackingPath path(filesystem, preferred_mountpoints);
  if (path.get() == NULL) {
    log_error(gc, init)("Use -XX:ZPath to specify the path to a %s filesystem", filesystem);
    return -1;
  }

  // Try to create an anonymous file using the O_TMPFILE flag. Note that this
  // flag requires kernel >= 3.11. If this fails we fall back to open/unlink.
  const int fd_anon = open(path.get(), O_TMPFILE|O_EXCL|O_RDWR|O_CLOEXEC, S_IRUSR|S_IWUSR);
  if (fd_anon == -1) {
    ZErrno err;
    log_debug(gc, init)("Failed to create anonymous file in %s (%s)", path.get(),
                        (err == EINVAL ? "Not supported" : err.to_string()));
  } else {
    // Get inode number for anonymous file
    struct stat stat_buf;
    if (fstat(fd_anon, &stat_buf) == -1) {
      ZErrno err;
      log_error(gc, init)("Failed to determine inode number for anonymous file (%s)", err.to_string());
      return -1;
    }

    log_info(gc, init)("Heap backed by file: %s/#" UINT64_FORMAT, path.get(), (uint64_t)stat_buf.st_ino);

    return fd_anon;
  }

  log_debug(gc, init)("Falling back to open/unlink");

  // Create file name
  char filename[PATH_MAX];
  snprintf(filename, sizeof(filename), "%s/%s.%d", path.get(), name, os::current_process_id());

  // Create file
  const int fd = open(filename, O_CREAT|O_EXCL|O_RDWR|O_CLOEXEC, S_IRUSR|S_IWUSR);
  if (fd == -1) {
    ZErrno err;
    log_error(gc, init)("Failed to create file %s (%s)", filename, err.to_string());
    return -1;
  }

  // Unlink file
  if (unlink(filename) == -1) {
    ZErrno err;
    log_error(gc, init)("Failed to unlink file %s (%s)", filename, err.to_string());
    return -1;
  }

  log_info(gc, init)("Heap backed by file: %s", filename);

  return fd;
}

第二步就是把一个虚拟地址通过mmap映射到一块真正的物理地址。在这里就能看出3个视图的具体操作了。也就是把同一个地址映射3次,具体代码在

jdk11u/src/hotspot/os_cpu/linux_x86/gc/z/zPhysicalMemoryBacking_linux_x86.cpp

在map中我们看到对于同一地址调用了3次nap_view。这里要稍微提一点,map中接受的物理地址并不是真正的物理地址,而是ZGC管理的物理地址。下一节会讨论这个问题。map函数会把ZGC的物理地址转化成mark0,mark1和remapped视图对应的虚拟地址,其实处理方法非常简单,把最低42位的地址的那个元数据位的第43、44、45位或即得到不同视图里面的虚拟地址。

void ZPhysicalMemoryBacking::map(ZPhysicalMemory pmem, uintptr_t offset) const {
  if (ZUnmapBadViews) {
    // Only map the good view, for debugging only
    map_view(pmem, ZAddress::good(offset), AlwaysPreTouch);
  } else {
    // Map all views
    map_view(pmem, ZAddress::marked0(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::marked1(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::remapped(offset), AlwaysPreTouch);
  }
}

void ZPhysicalMemoryBacking::map_view(ZPhysicalMemory pmem, uintptr_t addr, bool pretouch) const {
  const size_t nsegments = pmem.nsegments();

  // Map segments
  for (size_t i = 0; i < nsegments; i++) {
    const ZPhysicalMemorySegment segment = pmem.segment(i);
    const size_t size = segment.size();
    const void* const res = mmap((void*)addr, size, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_SHARED, _file.fd(), segment.start());
    if (res == MAP_FAILED) {
      ZErrno err;
      map_failed(err);
    }

    // Advise on use of transparent huge pages before touching it
    if (ZLargePages::is_transparent()) {
      advise_view(addr, size);
    }

    // NUMA interleave memory before touching it
    ZNUMA::memory_interleave(addr, size);

    if (pretouch) {
      pretouch_view(addr, size);
    }

    addr += size;
  }
}

目前ZGC并不支持Windows平台,实际上Windows上也有对应的功能。可以通过Windows APIs CreateFileMapping/MapViewOfFileEx完成内存映射。

Good mask/bad mask

image.png

heap地址的两级管理

为了更加灵活的管理内存,ZGC也有物理内存管理和虚拟内存管理,并且还实现了物理内存和虚拟内存的映射关系。
在ZGC中常见的几个虚拟空间有[0~4TB),[4TB~8TB),[8TB~12TB),[16TB~20TB)都有使用。其中[0~4TB)对应的是Java的堆空间;[4TB~8TB),[8TB~12TB),[16TB~20TB)分别对应Mark0,Mark1和Remapped这三个视图。介绍到这里大家肯定有点混乱。就是这几个区到底有什么关系?我们先看下面这张图:

两级内存管理

从图中我们可以观察得到:

  • 4TB是理论上最大的堆空间,其大小受限于JVM参数;
  • 0~4TB的虚拟地址ZGC仅向操作系统通过mmap申请保留,并不会映射到真正的物理地址;
  • 由于物理内存页面大小受操作系统的管理,并且通常来说物理内存远少于虚拟内存,所以物理内存比ZGC的页面小,进一步的说一个ZGC的页面可能几个不连续的物理页面组成。
    实际上我们还有一下:
  • 操作系统管理的虚拟内存为Mark0,Mark1和Remapped三个空间,但他们对应同一物理空间,在ZGC中这3个空间在同一时间点有且仅有一个空间有效;
  • 3个空间的切换是由GC的不同阶段触发的,详见后文介绍;
  • 结合上文介绍的关于使用mmap把同一物理地址映射到不同空间的优点,我们进一步推断这3个虚拟空间是为了实现不同阶段快速访问内存,以及发现不同阶段处理的不同对象。

页面设计

ZGC中支持3种页面,分别为小、中、大。其中小页面指的是2MB的虚拟空间,中页面是32MB的页面空间,大页面是受操作系统控制的。
标准大页Huge pages是Linux Kernel 2.6引入的,目的是通过使用大页内存来取代传统的4KB内存页面,以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。它有两种格式大小: 2MB和1GB,2MB页块大小适合用于GB大小的内存,1GB页块大小适合用于TB 级别的内存;2MB是默认的大页大小。
透明大页Transparent Huge Pages(THP),这个是RHEL 6开始引入的一个功能,在Linux6上透明大页是默认启用的。由于Huge pages 很难手动管理,而且通常需要对代码进行重大的更改才能有效的使用,因此RHEL 6开始引入了THP,它是一个抽象层,能够自动创建、管理和使用传统大页。
在ZGC中不同对象的大小会使用不同的页面类型。下面是ZGC页面大小,对象大小和对象对齐数据:

image.png

其中MinObjectAlignmentInBytes的缺省值8,它由参数ObjectAlignmentInBytes控制,大小在[8~256]之间,且为2的幂次。

如何开启大页面
TBD

ZGC如何管理虚拟内存

根据上面的介绍,ZGC的内存划分几个不同的区域,其中Mark0,Mark1,Remapped这三个区的地址关联同一物理地址。而对于0~4TB区域的管理,ZGC为了分配效率和垃圾回收效率,原则是:小对象分配在小页面,中等对象分配在中页面,大对象分配在大页面;小页分配在虚拟内存的头部、并且优先回收,中页和大页从虚拟内存的尾部、且尽量不回收。为什么这么设计?原因很简单大对象在垃圾回收时成本非常高,特别是大对象的移动非常耗时,所以尽量不要移动大对象。

NUMA

过去,x86系统中的所有内存都可以通过CPU进行同等访问。无论任何CPU执行操作,访问时间都相等,这也被称为“统一内存访问”(UMA,Uniform Memory Access)。
最近使用的x86处理器已经不再采取这一行为。在非统一内存访问(NUMA,Non-Uniform Memory Access)中,系统内存被划分到NUMA节点(node),并且与socket 相对应,或与特定某一组与本地系统内存子集具有相同访问延迟的CPU相对应。

源码解析

在通常情况下,我们会设置推空间的最大值和最小值,在ZGC中我们仍然可以通过xmx设置最大堆空间,xms设置最小堆空间。当然如果我们没有设置,JVM会启发式推断设置多少的堆空间合适。

内存管理器ZHeap

ZGC中堆空间的初始化在ZCollectionHeap中,这个类是ZGC的入口类,它override了一些关键的函数,比如:
对象分配相关的:
ZCollectedHeap::allocate_new_tlab,快速分配,。TLAB块分配,我们知道JVM中使用一种撞针分配法,即为每个线程分配一个TLAB块,然后在分配小对象时优先在TLAB中分配,就可以做到让多个线程无冲突的分配,加快了分配的速度。
ZCollectedHeap::mem_allocate,慢速分配。
当TLAB中不能分配对象时会调用这个函数分配内存。
在ZGC中做法有些不同,快速分配和慢速分配实际上调用的同样的代码。那么ZGC中不需要快速分配了吗?还是说把慢速分配变成了快速分配一样的?ZGC中实现了另外一套对象管理的方法,在这套方法中还是有快速分配和慢速分配的。
垃圾回收相关:
ZCollectedHeap::collect,垃圾回收。实际上ZGC是主动式垃圾回收,这个接口只有在显示调用垃圾回收才会用到。

另外在这个类中还有一个很关键的工作就是初始化ZHeap对象。它是ZGC内存管理者。它里面字段有:

ZWorkers _workers;并发、并行工作线程
ZObjectAllocator _object_allocator;对象分配器
ZPageAllocator _page_allocator;页面分配器
ZPageTable _pagetable;页表
ZMark _mark;标记管理器
ZReferenceProcessor _reference_processor; 引用处理
ZWeakRootsProcessor _weak_roots_processor; 弱根处理
ZRelocate _relocate;转移管理器
ZRelocationSet _relocation_set;转移集合
ZServiceability _serviceability;

初始化在构造函数中。


ZHeap::ZHeap() :
    _workers(),
    _object_allocator(_workers.nworkers()),
    _page_allocator(heap_min_size(), heap_max_size(), heap_max_reserve_size()),
    _pagetable(),
    _mark(&_workers, &_pagetable),
    _reference_processor(&_workers),
    _weak_roots_processor(&_workers),
    _relocate(&_workers),
    _relocation_set(),
    _serviceability(heap_min_size(), heap_max_size()) {
  // Install global heap instance
  assert(_heap == NULL, "Already initialized");
  _heap = this;

  // Update statistics
  ZStatHeap::set_at_initialize(heap_max_size(), heap_max_reserve_size());
}


size_t ZHeap::heap_min_size() const {
  const size_t aligned_min_size = align_up(InitialHeapSize, ZPageSizeMin);
  return MIN2(aligned_min_size, heap_max_size());
}

size_t ZHeap::heap_max_size() const {
  const size_t aligned_max_size = align_up(MaxHeapSize, ZPageSizeMin);
  return MIN2(aligned_max_size, ZAddressOffsetMax);
}

size_t ZHeap::heap_max_reserve_size() const {
  // Reserve one small page per worker plus one shared medium page. This is still just
  // an estimate and doesn't guarantee that we can't run out of memory during relocation.
  const size_t max_reserve_size = (_workers.nworkers() * ZPageSizeSmall) + ZPageSizeMedium;
  return MIN2(max_reserve_size, heap_max_size());
}

对象分配

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

  instanceOop i;

  i = (instanceOop)Universe::heap()->obj_allocate(this, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

```cpp
oop CollectedHeap::obj_allocate(Klass* klass, int size, TRAPS) {
  ObjAllocator allocator(klass, size, THREAD);
  return allocator.allocate();
}

HeapWord* MemAllocator::mem_allocate(Allocation& allocation) const {
  if (UseTLAB) {
    HeapWord* result = allocate_inside_tlab(allocation);
    if (result != NULL) {
      return result;
    }
  }

  return allocate_outside_tlab(allocation);
}

oop MemAllocator::allocate() const {
  oop obj = NULL;
  {
    Allocation allocation(*this, &obj);
    HeapWord* mem = mem_allocate(allocation);
    if (mem != NULL) {
      obj = initialize(mem);
    }
  }
  return obj;
}
HeapWord* ZCollectedHeap::allocate_new_tlab(size_t min_size, size_t requested_size, size_t* actual_size) {
  const size_t size_in_bytes = ZUtils::words_to_bytes(align_object_size(requested_size));
  const uintptr_t addr = _heap.alloc_tlab(size_in_bytes);

  if (addr != 0) {
    *actual_size = requested_size;
  }

  return (HeapWord*)addr;
}

HeapWord* ZCollectedHeap::mem_allocate(size_t size, bool* gc_overhead_limit_was_exceeded) {
  const size_t size_in_bytes = ZUtils::words_to_bytes(align_object_size(size));
  return (HeapWord*)_heap.alloc_object(size_in_bytes);
}


inline uintptr_t ZHeap::alloc_tlab(size_t size) {
  guarantee(size <= max_tlab_size(), "TLAB too large");
  return _object_allocator.alloc_object(size);
}

inline uintptr_t ZHeap::alloc_object(size_t size) {
  uintptr_t addr = _object_allocator.alloc_object(size);
  assert(ZAddress::is_good_or_null(addr), "Bad address");

  if (addr == 0) {
    out_of_memory();
  }

  return addr;
}
ZGC两种分配方式
HeapWord* ZCollectedHeap::allocate_new_tlab(size_t min_size, size_t requested_size, size_t* actual_size) {
  const size_t size_in_bytes = ZUtils::words_to_bytes(align_object_size(requested_size));
  const uintptr_t addr = _heap.alloc_tlab(size_in_bytes);

  if (addr != 0) {
    *actual_size = requested_size;
  }

  return (HeapWord*)addr;
}

HeapWord* ZCollectedHeap::mem_allocate(size_t size, bool* gc_overhead_limit_was_exceeded) {
  const size_t size_in_bytes = ZUtils::words_to_bytes(align_object_size(size));
  return (HeapWord*)_heap.alloc_object(size_in_bytes);
}

inline uintptr_t ZHeap::alloc_tlab(size_t size) {
  guarantee(size <= max_tlab_size(), "TLAB too large");
  return _object_allocator.alloc_object(size);
}

inline uintptr_t ZHeap::alloc_object(size_t size) {
  uintptr_t addr = _object_allocator.alloc_object(size);
  assert(ZAddress::is_good_or_null(addr), "Bad address");

  if (addr == 0) {
    out_of_memory();
  }

  return addr;
}
对象分配管理器
uintptr_t ZObjectAllocator::alloc_object(size_t size, ZAllocationFlags flags) {
  if (size <= ZObjectSizeLimitSmall) {
    // Small
    return alloc_small_object(size, flags);
  } else if (size <= ZObjectSizeLimitMedium) {
    // Medium
    return alloc_medium_object(size, flags);
  } else {
    // Large
    return alloc_large_object(size, flags);
  }
}

uintptr_t ZObjectAllocator::alloc_small_object(size_t size, ZAllocationFlags flags) {
  if (flags.worker_thread()) {
    return alloc_small_object_from_worker(size, flags);
  } else {
    return alloc_small_object_from_nonworker(size, flags);
  }
}

uintptr_t ZObjectAllocator::alloc_small_object_from_worker(size_t size, ZAllocationFlags flags) {
  assert(ZThread::is_worker(), "Should be a worker thread");

  ZPage* page = _worker_small_page.get();
  uintptr_t addr = 0;

  if (page != NULL) {
    addr = page->alloc_object(size);
  }

  if (addr == 0) {
    // Allocate new page
    page = alloc_page(ZPageTypeSmall, ZPageSizeSmall, flags);
    if (page != NULL) {
      addr = page->alloc_object(size);
    }
    _worker_small_page.set(page);
  }

  return addr;
}

流程图如下

image.png

页面管理器ZPageAllocator

页面管理是ZGC中关于内存管理最关键的模块。它的主要字段有:
ZVirtualMemoryManager _virtual; 虚拟地址管理器
ZPhysicalMemoryManager _physical;物理地址管理器
ZPageCache _cache;页面缓存
const size_t _max_reserve;内存最大保留数
ZPreMappedMemory _pre_mapped;页面预分配映射
ZList<ZPageAllocRequest> _queue;分队列
ZList<ZPage> _detached;

初始化在构造函数中。

ZPageAllocator::ZPageAllocator(size_t min_capacity, size_t max_capacity, size_t max_reserve) :
_lock(),
_virtual(),
_physical(max_capacity, ZPageSizeMin),
_cache(),
_max_reserve(max_reserve),
_pre_mapped(_virtual, _physical, try_ensure_unused_for_pre_mapped(min_capacity)),
_used_high(0),
_used_low(0),
_used(0),
_allocated(0),
_reclaimed(0),
_queue(),
_detached() {}

我们重点关注虚拟内存管理,物理内存管理。虚拟地址管理器申请[0, 4TB)作为保留内存。物理内存管理器仅仅是把GC使用的最大的内存Xmx传递进来,用于控制物理内存分配。

虚拟地址管理器
ZVirtualMemoryManager::ZVirtualMemoryManager() :
    _manager(),
    _initialized(false) {
  // Reserve address space
  if (!reserve(ZAddressSpaceStart, ZAddressSpaceSize)) {
    return;
  }

  // Make the complete address view free
  _manager.free(0, ZAddressOffsetMax);

  // Register address space with native memory tracker
  nmt_reserve(ZAddressSpaceStart, ZAddressSpaceSize);

  // Successfully initialized
  _initialized = true;
}

bool ZVirtualMemoryManager::reserve(uintptr_t start, size_t size) {
  // Reserve address space
  const uintptr_t actual_start = (uintptr_t)mmap((void*)start, size, PROT_NONE,
                                                 MAP_ANONYMOUS|MAP_PRIVATE|MAP_NORESERVE, -1, 0);
  if (actual_start != start) {
    log_error(gc)("Failed to reserve address space for Java heap");
    return false;
  }

  return true;
}

ZVirtualMemory ZVirtualMemoryManager::alloc(size_t size, bool alloc_from_front) {
  uintptr_t start;

  if (alloc_from_front || size <= ZPageSizeSmall) {
    // Small page
    start = _manager.alloc_from_front(size);
  } else {
    // Medium/Large page
    start = _manager.alloc_from_back(size);
  }

  return ZVirtualMemory(start, size);
}
物理内存管理
ZPhysicalMemory::ZPhysicalMemory(size_t size) :
    _nsegments(0),
    _segments(NULL) {
  add_segment(ZPhysicalMemorySegment(0, size));
}

void ZPhysicalMemory::add_segment(ZPhysicalMemorySegment segment) {
  // 物理内存比较简单,只支持线性分配。
  // 在添加segment时当发现要添加的segment和最后一个segment可以合并时
  if (_nsegments > 0) {
    ZPhysicalMemorySegment& last = _segments[_nsegments - 1];
    assert(last.end() <= segment.start(), "Segments added out of order");
    if (last.end() == segment.start()) {
      // Merge
      last.expand(segment.size());
      return;
    }
  }

  // 通常只有第一次初始化时需要分配资源,后续的物理内存分配都是走上面的分支
  // 所以实际上ZGC的物理内存只有一个大的segment
  const size_t size = sizeof(ZPhysicalMemorySegment) * (_nsegments + 1);
  _segments = (ZPhysicalMemorySegment*)ReallocateHeap((char*)_segments, size, mtGC);

  _segments[_nsegments] = segment;
  _nsegments++;
}

ZPhysicalMemory::ZPhysicalMemory(const ZPhysicalMemorySegment& segment) :
    _nsegments(0),
    _segments(NULL) {
  add_segment(segment);
}

ZPhysicalMemory ZPhysicalMemoryBacking::alloc(size_t size) {
  assert(is_aligned(size, _granule_size), "Invalid size");

  ZPhysicalMemory pmem;

  // Allocate segments
  for (size_t allocated = 0; allocated < size; allocated += _granule_size) {
    const uintptr_t start = _manager.alloc_from_front(_granule_size);
    assert(start != UINTPTR_MAX, "Allocation should never fail");
    pmem.add_segment(ZPhysicalMemorySegment(start, _granule_size));
  }

  return pmem;
}

ZGC的物理地址并不是操作系统中的物理地址,从概念上它更实际上类似虚拟地址,它是为了管理mutator物理地址的使用,怎么理解这句话呢?ZGC中内存空间的分配是以page为粒度(实际上是以最小page 2MB为粒度)。ZGC为了减少频繁的请求/释放物理内存,所以设计了物理内存,物理内存实际上仅仅记录的是地址的使用情况。

image.png

ZGC中物理内存管理的基本单位是segment,它包含start和end。每一个segment都是2MB。所以可能会存在一个超大的对象10MB,则它需要5个segment。在ZGC和操作系统的交互时,将分成5次向操作系统申请内存,所以可以把一个大对象分配在操作系统层面不连续的物理空间中。但是在物理内存管理中,在把这些segment进行合并。所以在实际中很有可能只有一个segment。
但是ZGC管理的时候以Page为单位,这时就会存在一个page,包含了5个segments共同组成这个对象。
segment是ZGC向操作系统请求内存的基本单位;Page是对象内存管理的单位。

内存预映射管理
ZPreMappedMemory::ZPreMappedMemory(ZVirtualMemoryManager &vmm, ZPhysicalMemoryManager &pmm, size_t size) :
    _vmem(),
    _pmem(),
    _initialized(false) {
  if (!vmm.is_initialized() || !pmm.is_initialized()) {
    // Not initialized
    return;
  }

  // Pre-mapping and pre-touching memory can take a long time. Log a message
  // to help the user understand why the JVM might seem slow to start.
  log_info(gc, init)("Pre-touching: %s", AlwaysPreTouch ? "Enabled" : "Disabled");
  log_info(gc, init)("Pre-mapping: " SIZE_FORMAT "M", size / M);

  if (size > 0) {
    _pmem = pmm.alloc(size);
    if (_pmem.is_null()) {
      // Out of memory
      log_error(gc, init)("Failed to pre-map Java heap (Cannot allocate physical memory)");
      return;
    }

    _vmem = vmm.alloc(size, true /* alloc_from_front */);
    if (_vmem.is_null()) {
      // Out of address space
      log_error(gc, init)("Failed to pre-map Java heap (Cannot allocate virtual memory)");
      pmm.free(_pmem);
      return;
    }

    // Map physical memory
    pmm.map(_pmem, _vmem.start());
  }

  _initialized = true;
}
页面分配管理器申请新的页面

Alloc——page的流程大体如下:

优先从cache中分配,当page可以回收时加入到cache中
从预分配的内存中分配,预分配Premap是在ZGC启动时按照最小heap的需求分配的内存,如果设置了xms,一般就是设置的值
如果以上两种情况都无法成功分配,则通过create_page向ZGC内存管理器申请新的内存page,当然在申请的时候不能超过堆的最大可用值,在create——page申请新的page之后,会真正的向操作系统申请物理内存,此时会调用mmap完成内存映射。

在进入alloc_page之前会更加内存的控制策略来设置分配是阻塞还是非阻塞。当设置参数ZStallOnOutOfMemory为false时进行阻塞分配,缺省值为ture,即非阻塞分配。两者的区别在于阻塞分配一定会等着GC完成内存的分配直到成功,而非阻塞分配则是在内存不足时GC无法分配内存抛出OutOfMemoryError异常。
在从操作系统中申请内存的时候需要加锁,因为只有一个heap。

page和它的状态介绍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • 又是一年秋招季,哎呀妈呀我被虐的惨来~这不,前几阵失踪没更新博客,其实是我偷偷把时间用在复习课本了(雾 坚持在社区...
    tengshe789阅读 1,918评论 0 8
  • 在linux下,使用top,free等命令查看系统或者进程的内存使用情况时,经常看到buff/cache meme...
    analanxingde阅读 692评论 0 2
  • 操作系统对内存的管理 没有内存抽象的年代 在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物...
    Mr槑阅读 16,566评论 3 24
  • 前言 内存管理一向是所有操作系统书籍不惜笔墨重点讨论的内容,无论市面上或是网上都充斥着大量涉及内存管理的教材和资料...
    木有sky阅读 878评论 0 1
  • 我见过清澈的诗句 清澈的回忆 清澈的目光 相遇 没见过清澈的人 偶尔有一双眼睛 从人群深处 闪烁出来 人们说 那样...
    段童阅读 211评论 0 2