OpenJDK16 ZGC 源码分析(一)内存管理

1. 概览

ZGC在JDK11中作为实验性功能引入后,已经经过了5个版本的演进,目前较之前版本有了较大的变化。
本文及其后几篇文章,将分析ZGC的设计思想和原理。

关于ZGC的介绍,可以参考OpenJDK ZGC 源码分析(一)概览

2. 简介

  • ZGC为了支持TB级内存,采用了基于Page的分页管理(类似于G1的Region)。
  • 同时,为了加快内存访问速度,快速的进行并发标记和relocate,ZGC新引入了Color Pointers;Color Pointers与Shenandoah GC使用的Brooks Pointers机制不同,依赖内核提供的多视图映射,因此仅能支持部分操作系统的64位版本,适用性不如Shenandoah GC,同时也无法支持指针压缩CompressedOops。
  • 另外,为了高效内存管理,设计了两级内存管理系统。

3. 指针结构

zGlobals_x86.cpp

// Address Space & Pointer Layout 3
// --------------------------------
//
//  +--------------------------------+ 0x00007FFFFFFFFFFF (127TB)
//  .                                .
//  .                                .
//  .                                .
//  +--------------------------------+ 0x0000500000000000 (80TB)
//  |         Remapped View          |
//  +--------------------------------+ 0x0000400000000000 (64TB)
//  .                                .
//  +--------------------------------+ 0x0000300000000000 (48TB)
//  |         Marked1 View           |
//  +--------------------------------+ 0x0000200000000000 (32TB)
//  |         Marked0 View           |
//  +--------------------------------+ 0x0000100000000000 (16TB)
//  .                                .
//  +--------------------------------+ 0x0000000000000000
//
//   6               4  4  4 4
//   3               8  7  4 3                                               0
//  +------------------+----+-------------------------------------------------+
//  |00000000 00000000 |1111|1111 11111111 11111111 11111111 11111111 11111111|
//  +------------------+----+-------------------------------------------------+
//  |                  |    |
//  |                  |    * 43-0 Object Offset (44-bits, 16TB address space)
//  |                  |
//  |                  * 47-44 Metadata Bits (4-bits)  0001 = Marked0      (Address view 16-32TB)
//  |                                                  0010 = Marked1      (Address view 32-48TB)
//  |                                                  0100 = Remapped     (Address view 64-80TB)
//  |                                                  1000 = Finalizable  (Address view N/A)
//  |
//  * 63-48 Fixed (16-bits, always zero)
//
  • ZGC指针布局有三种方式,分别用于支持4TB、8TB、16TB的堆空间,以上代码用于为layout 3支持16TB的布局;
  • 43-0 bit 对象地址
  • 47-44 对象视图,分为三种对象视图
    • Marked0、Marked1
    • Remapped
  • x86和aarch64架构下最多仅支持48位指针,主要是因为硬件限制。通常为了节约成本,64位处理器地址线一般仅40-50条,因此寻址范围远不及64位的理论值。

4. 多视图

ZGC将同一段物理内存映射到3个不同的虚拟内存视图,分别为Marked0、Marked1、Remapped,这即是ZGC中的Color Pointers,通过Color Pointers区分不同的GC阶段。

4.1 映射

ZGC的多视图映射依赖于内核提供的mmap方法,具体代码如下
zPhysicalMemory.hpp, zPhysicalMemory.cpp, zPhysicalMemoryBacking_linux.cpp

// 物理内存管理类
class ZPhysicalMemory {
private:
  ZArray<ZPhysicalMemorySegment> _segments;

  void insert_segment(int index, uintptr_t start, size_t size, bool committed);
  void replace_segment(int index, uintptr_t start, size_t size, bool committed);
  void remove_segment(int index);

public:
  ZPhysicalMemory();
  ZPhysicalMemory(const ZPhysicalMemorySegment& segment);
  ZPhysicalMemory(const ZPhysicalMemory& pmem);
  const ZPhysicalMemory& operator=(const ZPhysicalMemory& pmem);

  bool is_null() const;
  size_t size() const;

  int nsegments() const;
  const ZPhysicalMemorySegment& segment(int index) const;

  void add_segments(const ZPhysicalMemory& pmem);
  void remove_segments();

  void add_segment(const ZPhysicalMemorySegment& segment);
  bool commit_segment(int index, size_t size);
  bool uncommit_segment(int index, size_t size);

  ZPhysicalMemory split(size_t size);
  ZPhysicalMemory split_committed();
};

// 将三个虚拟内存视图映射到同一物理内存
// 在JDK14中增加了对于ZVerifyViews JVM参数的支持(https://bugs.openjdk.java.net/browse/JDK-8232604)
void ZPhysicalMemoryManager::map(uintptr_t offset, const ZPhysicalMemory& pmem) const {
  const size_t size = pmem.size();

  if (ZVerifyViews) {
    // Map good view
    map_view(ZAddress::good(offset), pmem);
  } else {
    // Map all views
    map_view(ZAddress::marked0(offset), pmem);
    map_view(ZAddress::marked1(offset), pmem);
    map_view(ZAddress::remapped(offset), pmem);
  }

  nmt_commit(offset, size);
}

void ZPhysicalMemoryManager::map_view(uintptr_t addr, const ZPhysicalMemory& pmem) const {
  size_t size = 0;

  // 逐个映射物理内存
  // ZGC中使用segment管理物理内存,后续文章将详细介绍
  for (int i = 0; i < pmem.nsegments(); i++) {
    const ZPhysicalMemorySegment& segment = pmem.segment(i);
    _backing.map(addr + size, segment.size(), segment.start());
    size += segment.size();
  }

  // Setup NUMA interleaving for large pages
  if (ZNUMA::is_enabled() && ZLargePages::is_explicit()) {
    // To get granule-level NUMA interleaving when using large pages,
    // we simply let the kernel interleave the memory for us at page
    // fault time.
    os::numa_make_global((char*)addr, size);
  }
}

// 最终对于map的调用
// 对于linux系统,调用mmap进行映射
void ZPhysicalMemoryBacking::map(uintptr_t addr, size_t size, uintptr_t offset) const {
  // 可读、可写、修改共享
  // 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。
  const void* const res = mmap((void*)addr, size, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_SHARED, _fd, offset);
  if (res == MAP_FAILED) {
    ZErrno err;
    fatal("Failed to map memory (%s)", err.to_string());
  }
}
  • ZPhysicalMemory是ZGC对于物理内存管理的抽象,收敛ZGC对于物理内存的访问。
  • ZPhysicalMemory底层根据宿主操作系统调用不同的ZPhysicalMemoryBacking实现,进行多视图映射。

4.2 物理内存管理

ZGC对于物理内存的管理主要在ZPhysicalMemory类中,此处需要注意,ZGC上下文中的物理内存,不是真正的物理内存,而是操作系统虚拟内存。
在这里插入图片描述

ZGC中管理物理内存的基本单位是segment。segment默认与small page size一样,都是2MB。引入segment是为了避免频繁的申请和释放内存的系统调用,一次申请2MB,当segment空闲时,将加入空闲列表,等待之后重复使用。
zGlobals_x86.hpp

// 默认page size偏移量
const size_t ZPlatformGranuleSizeShift = 21; // 2MB

ZPhysicalMemorySegment是ZGC对于物理内存segment的抽象,定义如下:
zPhysicalMemory.cpp

class ZPhysicalMemorySegment : public CHeapObj<mtGC> {
private:
  // 开始偏移量
  uintptr_t _start;
  // 开始偏移量+size
  uintptr_t _end;
  bool      _committed;

public:
  ZPhysicalMemorySegment();
  ZPhysicalMemorySegment(uintptr_t start, size_t size, bool committed);

  uintptr_t start() const;
  uintptr_t end() const;
  size_t size() const;

  bool is_committed() const;
  void set_committed(bool committed);
};

5. 页面管理

5.1 Page介绍

ZGC中内存管理的基本单元是Page(类似于G1中的region),ZGC有3种不同的页面类型:小型(2MB),中型(32MB)和大型(2MB的倍数)。
zGlobals_x86.hpp

const size_t ZPlatformGranuleSizeShift = 21; // 2MB

zGlobals.hpp

// Page types
const uint8_t     ZPageTypeSmall                = 0;  
const uint8_t     ZPageTypeMedium               = 1;
const uint8_t     ZPageTypeLarge                = 2;

// Page size shifts
const size_t      ZPageSizeSmallShift           = ZGranuleSizeShift;
extern size_t     ZPageSizeMediumShift;

// Page sizes
// small page 2MB
const size_t      ZPageSizeSmall                = (size_t)1 << ZPageSizeSmallShift;

extern size_t     ZPageSizeMedium;

// 对象size限制,small page不超过2MB/8, 256KB
const size_t      ZObjectSizeLimitSmall         = ZPageSizeSmall / 8; // 12.5% max waste
extern size_t     ZObjectSizeLimitMedium;

medium页size的计算方法如下:
zHeuristics.cpp

void ZHeuristics::set_medium_page_size() {
  // Set ZPageSizeMedium so that a medium page occupies at most 3.125% of the
  // max heap size. ZPageSizeMedium is initially set to 0, which means medium
  // pages are effectively disabled. It is adjusted only if ZPageSizeMedium
  // becomes larger than ZPageSizeSmall.
  const size_t min = ZGranuleSize;
  const size_t max = ZGranuleSize * 16;
  const size_t unclamped = MaxHeapSize * 0.03125;
  const size_t clamped = clamp(unclamped, min, max);
  const size_t size = round_down_power_of_2(clamped);

  if (size > ZPageSizeSmall) {
    // Enable medium pages
    ZPageSizeMedium             = size;
    ZPageSizeMediumShift        = log2_intptr(ZPageSizeMedium);
    ZObjectSizeLimitMedium      = ZPageSizeMedium / 8;
    ZObjectAlignmentMediumShift = (int)ZPageSizeMediumShift - 13;
    ZObjectAlignmentMedium      = 1 << ZObjectAlignmentMediumShift;
  }
}
  • 取堆最大容量(Xmx)的0.03125 unclamped
  • 如果unclamped在2MB到32MB之间,clamped赋值unclamped;如果unclamped小于2MB,则clamped赋值2MB;如果unclamped大于32MB,则clamped赋值32MB
  • 向下取clamped最接近的2的幂数,即为medium页size
  • 考虑到目前的硬件环境,通常的medium页size为32MB
  • ZObjectSizeLimitMedium为ZPageSizeMedium / 8,则通常情况下,medium页的对象size限制为4MB。超过4MB的对象需要放入large页

对于large page的处理如下:
zObjectAllocator.cpp

uintptr_t ZObjectAllocator::alloc_large_object(size_t size, ZAllocationFlags flags) {
  uintptr_t addr = 0;

  // Allocate new large page
  const size_t page_size = align_up(size, ZGranuleSize);
  ZPage* const page = alloc_page(ZPageTypeLarge, page_size, flags);
  if (page != NULL) {
    // Allocate the object
    addr = page->alloc_object(size);
  }

  return addr;
}
  • 分配大对象时,触发分配large page
  • 对齐大对象size到2MB的倍数后分配large page

zObjectAllocator.cpp

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);
  }
}
  • 当对象size大于medium页对象size限制时,触发大对象分配。
  • 因此,large页的实际size很可能小于medium页size。

5.2 Page的分配

Page分配的入口在ZHeap的alloc_page方法:
zHeap.cpp

ZPage* ZObjectAllocator::alloc_page(uint8_t type, size_t size, ZAllocationFlags flags) {
  // 调用了page分配器的alloc_page函数
  ZPage* const page = ZHeap::heap()->alloc_page(type, size, flags);
  if (page != NULL) {
    // 增加使用内存数
    Atomic::add(_used.addr(), size);
  }

  return page;
}

zPageAllocator.cpp

ZPage* ZPageAllocator::alloc_page(uint8_t type, size_t size, ZAllocationFlags flags) {
  EventZPageAllocation event;

retry:
  ZPageAllocation allocation(type, size, flags);

  // 从page cache分配page
  // 如果分配成功,调用alloc_page_finalize完成分配
  // 分配过程中,如果是阻塞模式,有可能在安全点被阻塞
  if (!alloc_page_or_stall(&allocation)) {
    // Out of memory
    return NULL;
  }

  // 如果从page cache分配失败,则从物理内存申请页
  // 提交page
  ZPage* const page = alloc_page_finalize(&allocation);
  if (page == NULL) {
    // 如果commit或者map失败,则goto到retry,重新分配
    alloc_page_failed(&allocation);
    goto retry;
  }

  // ...
  // ...
  // ...
  return page;
}

bool ZPageAllocator::alloc_page_or_stall(ZPageAllocation* allocation) {
  {
    // 分配page需要上锁,因为只有一个堆
    ZLocker<ZLock> locker(&_lock);

    // 分配成功,返回true
    if (alloc_page_common(allocation)) {
      return true;
    }

    // 如果是非阻塞模式,返回false
    if (allocation->flags().non_blocking()) {
      return false;
    }

    // 分配请求入队,等待GC完成
    _stalled.insert_last(allocation);
  }

  return alloc_page_stall(allocation);
}

// 阻塞分配,等待GC
bool ZPageAllocator::alloc_page_stall(ZPageAllocation* allocation) {
  ZStatTimer timer(ZCriticalPhaseAllocationStall);
  EventZAllocationStall event;
  ZPageAllocationStall result;

  // 检查虚拟机是否已经完成初始化
  check_out_of_memory_during_initialization();

  do {
    // 启动异步GC
    ZCollectedHeap::heap()->collect(GCCause::_z_allocation_stall);

    // 挂起,等待GC结果
    result = allocation->wait();
  } while (result == ZPageAllocationStallStartGC);

  // ...
  // ...
  // ...
  return (result == ZPageAllocationStallSuccess);
}
  • 阻塞分配与非阻塞分配,由系统参数ZStallOnOutOfMemory控制,默认阻塞分配。阻塞分配时,如果分配失败,则触发GC,等待GC结束后再次分配,直到分配成功。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值