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结束后再次分配,直到分配成功。