ZGC源碼分析(2)- ZGC內(nèi)存管理

ZGC為了支持TB級(jí)內(nèi)存,設(shè)計(jì)了分頁(yè)管理(類似于G1的分區(qū));為了能夠快速的進(jìn)行并發(fā)標(biāo)記和并發(fā)移動(dòng),對(duì)內(nèi)存空間重新進(jìn)行了劃分,這就是Color Pointers,同時(shí)設(shè)計(jì)了物理內(nèi)存和虛擬內(nèi)存兩級(jí)內(nèi)存管理。注意:這里的虛擬內(nèi)存和操作系統(tǒng)的虛擬內(nèi)存概念是一樣的;但是物理內(nèi)存和操作系統(tǒng)中的物理內(nèi)存并不一樣,ZGC是借用了物理地址這個(gè)概念;但是ZGC中物理地址和操作系統(tǒng)的物理地址是映射關(guān)聯(lián)的,所以為了清晰的了解ZGC內(nèi)存管理,我們有必要先了解一下操作系統(tǒng)的虛擬內(nèi)存和物理內(nèi)存。

操作系統(tǒng)地址管理

物理內(nèi)存非常直觀,就是真實(shí)存在的,插在主板內(nèi)存槽上的內(nèi)存條的容量的大小。我們經(jīng)常所說(shuō)的一臺(tái)計(jì)算機(jī)的配置有1GB或者2GB內(nèi)存,這個(gè)就是真實(shí)的物理內(nèi)存。而虛擬內(nèi)存是伴隨著操作系統(tǒng)和硬件的發(fā)展出現(xiàn)的。
虛擬地址是操作系統(tǒng)根據(jù)CPU的尋址能力,支持訪問(wèn)的虛擬空間,比如前些年大家使用的32位系統(tǒng),對(duì)應(yīng)的虛擬地址空間為0-2^32,即0-4G,而我們的計(jì)算機(jī)的物理內(nèi)存可能只有512MB。虛擬內(nèi)存的發(fā)展解決了很多問(wèn)題,也帶了很多好處。具體可以參考《深入理解計(jì)算機(jī)系統(tǒng)結(jié)構(gòu)》
當(dāng)程序試圖訪問(wèn)一個(gè)虛存頁(yè)面時(shí),這個(gè)請(qǐng)求會(huì)通過(guò)操作系統(tǒng)來(lái)訪問(wèn)真正的內(nèi)存,首先到頁(yè)表中去查詢?cè)擁?yè)是否已映射到物理頁(yè)框中,并記錄在頁(yè)表中。如果在,則會(huì)通過(guò)MMU把頁(yè)碼轉(zhuǎn)換成頁(yè)框碼,并加上虛擬地址提供的頁(yè)內(nèi)偏移量形成物理地址后去訪問(wèn)物理內(nèi)存;如果不在,則意味著該虛存頁(yè)面還沒(méi)有被載入內(nèi)存,這時(shí)MMU就會(huì)通知操作系統(tǒng):發(fā)生了一個(gè)頁(yè)面訪問(wèn)錯(cuò)誤(頁(yè)面錯(cuò)誤),接下來(lái)系統(tǒng)會(huì)啟動(dòng)所謂的“請(qǐng)頁(yè)”機(jī)制,即調(diào)用相應(yīng)的系統(tǒng)操作函數(shù),判斷該虛擬地址是否為有效地址。如果是有效的地址,就從虛擬內(nèi)存中將該地址指向的頁(yè)面讀入到內(nèi)存中的一個(gè)空閑頁(yè)框中,并在頁(yè)表中添加上相對(duì)應(yīng)的表項(xiàng),最后處理器將從發(fā)生頁(yè)面錯(cuò)誤的地方重新開(kāi)始運(yùn)行;如果是無(wú)效的地址,則表明進(jìn)程在試圖訪問(wèn)一個(gè)不存在的虛擬地址,此時(shí)操作系統(tǒng)將終止此次訪問(wèn)。當(dāng)然,也存在這樣的情況:在請(qǐng)頁(yè)成功之后,內(nèi)存中已沒(méi)有空閑物理頁(yè)框了。這是,系統(tǒng)必須啟動(dòng)所謂地“交換”機(jī)制,即調(diào)用相應(yīng)的內(nèi)核操作函數(shù),在物理頁(yè)框中尋找一個(gè)當(dāng)前不再使用或者近期可能不會(huì)用到的頁(yè)面所占據(jù)的頁(yè)框。找到后,就把其中的頁(yè)移出,以裝載新的頁(yè)面。對(duì)移出頁(yè)面根據(jù)兩種情況來(lái)處理:如果該頁(yè)未被修改過(guò),則刪除它;如果該頁(yè)曾經(jīng)被修改過(guò),則系統(tǒng)必須將該頁(yè)寫(xiě)回輔存。

關(guān)于虛擬地址更多的內(nèi)容可以參考其他的網(wǎng)頁(yè):比例https://www.cnblogs.com/shijingjing07/p/5611579.html

地址空間設(shè)計(jì)

ZGC目前林支持64位Linux,最多管理4TB的內(nèi)存。不知道你有沒(méi)有注意到這個(gè)地方似乎有點(diǎn)問(wèn)題,64位系統(tǒng)支持的內(nèi)存遠(yuǎn)超過(guò)4TB,那么為什么我們一直強(qiáng)調(diào)它只能支持4TB,為什么不使用更多的虛擬內(nèi)存?ZGC對(duì)整個(gè)內(nèi)存空間進(jìn)行劃分,這是來(lái)自于源碼中關(guān)于地址空間的一個(gè)說(shuō)明,如下:

image.png

簡(jiǎn)單來(lái)說(shuō),整個(gè)地址空間被分成3個(gè)視圖,分別是:Mark0,Mark1和Remapped。而且有意思的是這3個(gè)視圖會(huì)映射到操作系統(tǒng)的同一物理地址。這里就涉及到ZGC中Color Pointers的概念。
我們先來(lái)看一下地址空間的設(shè)計(jì),如何映射的,再分析一下為什么這么做以及這么做的優(yōu)點(diǎn)。
ZGC支持64位系統(tǒng),其中42位用于地址,4位用于描述元數(shù)據(jù),其實(shí)就是大家所說(shuō)的Color Pointers,還有1位目前暫時(shí)沒(méi)有使用,最高17位固定為0。具體如下圖:

image.png

由于42位地址最大的尋址空間就是4TB,這就是為什么ZGC一直宣稱自己最大支持4TB內(nèi)存的原因。這里還有視圖的概念,Mark0、Mark1和Remapped就是3個(gè)視圖,分別用第43、44、45位上設(shè)置1,就是對(duì)應(yīng)的視圖。所以這里的意思就是這4位標(biāo)記并不是用于地址尋址使用的。他們所使用的地址都是最低42位所對(duì)應(yīng)的地址。視圖是什么意思呢?簡(jiǎn)單的回答就是這3個(gè)虛擬地址映射到一個(gè)物理地址,而為了區(qū)分這三個(gè)虛擬地址,稱他們?yōu)橐晥D。下面我們看一下ZGC是如何進(jìn)行抵制映射創(chuàng)建視圖。

ZGC虛擬地址映射

熟悉Linux編程的人可以略過(guò)這一部分。把多個(gè)虛擬地址映射到一個(gè)物理地址的步驟可以總結(jié)如下:

  • 打開(kāi)一個(gè)文件描述,這個(gè)文件描述符可以是內(nèi)存文件描述符也可以是普通文件描述符(最好是內(nèi)存文件描述符)
  • 把多個(gè)地址使用mmap映射到這個(gè)文件描述符上

先看下如何創(chuàng)建文件描述,具體代碼

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

這個(gè)代碼有一個(gè)create_fd的函數(shù)就是創(chuàng)建文件描述符,優(yōu)先創(chuàng)建內(nèi)存文件描述符,如果不能成功則創(chuàng)建一個(gè)磁盤(pán)文件描述符。

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);
}

創(chuàng)建內(nèi)存文件描述符通過(guò)系統(tǒng)調(diào)用memfd_create完成,由于memfd_create是內(nèi)核態(tài)才能調(diào)用的函數(shù),所以必須通過(guò)syscall函數(shù)從用戶態(tài)進(jìn)入內(nèi)核態(tài),并傳遞參數(shù)__NR_memfd_create,最終操作系統(tǒng)調(diào)用相應(yīng)的函數(shù)完成。

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);
}

創(chuàng)建文件描述,這里要注意的是ZGC支持大頁(yè)面,所以會(huì)根據(jù)具體的文件系統(tǒng)信息創(chuàng)建不同的文件描述符。

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;
}

第二步就是把一個(gè)虛擬地址通過(guò)mmap映射到一塊真正的物理地址。在這里就能看出3個(gè)視圖的具體操作了。也就是把同一個(gè)地址映射3次,具體代碼在

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

在map中我們看到對(duì)于同一地址調(diào)用了3次nap_view。這里要稍微提一點(diǎn),map中接受的物理地址并不是真正的物理地址,而是ZGC管理的物理地址。下一節(jié)會(huì)討論這個(gè)問(wèn)題。map函數(shù)會(huì)把ZGC的物理地址轉(zhuǎn)化成mark0,mark1和remapped視圖對(duì)應(yīng)的虛擬地址,其實(shí)處理方法非常簡(jiǎn)單,把最低42位的地址的那個(gè)元數(shù)據(jù)位的第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平臺(tái),實(shí)際上Windows上也有對(duì)應(yīng)的功能??梢酝ㄟ^(guò)Windows APIs CreateFileMapping/MapViewOfFileEx完成內(nèi)存映射。

Good mask/bad mask

image.png

heap地址的兩級(jí)管理

為了更加靈活的管理內(nèi)存,ZGC也有物理內(nèi)存管理和虛擬內(nèi)存管理,并且還實(shí)現(xiàn)了物理內(nèi)存和虛擬內(nèi)存的映射關(guān)系。
在ZGC中常見(jiàn)的幾個(gè)虛擬空間有[0~4TB),[4TB~8TB),[8TB~12TB),[16TB~20TB)都有使用。其中[0~4TB)對(duì)應(yīng)的是Java的堆空間;[4TB~8TB),[8TB~12TB),[16TB~20TB)分別對(duì)應(yīng)Mark0,Mark1和Remapped這三個(gè)視圖。介紹到這里大家肯定有點(diǎn)混亂。就是這幾個(gè)區(qū)到底有什么關(guān)系?我們先看下面這張圖:

兩級(jí)內(nèi)存管理

從圖中我們可以觀察得到:

  • 4TB是理論上最大的堆空間,其大小受限于JVM參數(shù);
  • 0~4TB的虛擬地址ZGC僅向操作系統(tǒng)通過(guò)mmap申請(qǐng)保留,并不會(huì)映射到真正的物理地址;
  • 由于物理內(nèi)存頁(yè)面大小受操作系統(tǒng)的管理,并且通常來(lái)說(shuō)物理內(nèi)存遠(yuǎn)少于虛擬內(nèi)存,所以物理內(nèi)存比ZGC的頁(yè)面小,進(jìn)一步的說(shuō)一個(gè)ZGC的頁(yè)面可能幾個(gè)不連續(xù)的物理頁(yè)面組成。
    實(shí)際上我們還有一下:
  • 操作系統(tǒng)管理的虛擬內(nèi)存為Mark0,Mark1和Remapped三個(gè)空間,但他們對(duì)應(yīng)同一物理空間,在ZGC中這3個(gè)空間在同一時(shí)間點(diǎn)有且僅有一個(gè)空間有效;
  • 3個(gè)空間的切換是由GC的不同階段觸發(fā)的,詳見(jiàn)后文介紹;
  • 結(jié)合上文介紹的關(guān)于使用mmap把同一物理地址映射到不同空間的優(yōu)點(diǎn),我們進(jìn)一步推斷這3個(gè)虛擬空間是為了實(shí)現(xiàn)不同階段快速訪問(wèn)內(nèi)存,以及發(fā)現(xiàn)不同階段處理的不同對(duì)象。

頁(yè)面設(shè)計(jì)

ZGC中支持3種頁(yè)面,分別為小、中、大。其中小頁(yè)面指的是2MB的虛擬空間,中頁(yè)面是32MB的頁(yè)面空間,大頁(yè)面是受操作系統(tǒng)控制的。
標(biāo)準(zhǔn)大頁(yè)Huge pages是Linux Kernel 2.6引入的,目的是通過(guò)使用大頁(yè)內(nèi)存來(lái)取代傳統(tǒng)的4KB內(nèi)存頁(yè)面,以適應(yīng)越來(lái)越大的系統(tǒng)內(nèi)存,讓操作系統(tǒng)可以支持現(xiàn)代硬件架構(gòu)的大頁(yè)面容量功能。它有兩種格式大?。?2MB和1GB,2MB頁(yè)塊大小適合用于GB大小的內(nèi)存,1GB頁(yè)塊大小適合用于TB 級(jí)別的內(nèi)存;2MB是默認(rèn)的大頁(yè)大小。
透明大頁(yè)Transparent Huge Pages(THP),這個(gè)是RHEL 6開(kāi)始引入的一個(gè)功能,在Linux6上透明大頁(yè)是默認(rèn)啟用的。由于Huge pages 很難手動(dòng)管理,而且通常需要對(duì)代碼進(jìn)行重大的更改才能有效的使用,因此RHEL 6開(kāi)始引入了THP,它是一個(gè)抽象層,能夠自動(dòng)創(chuàng)建、管理和使用傳統(tǒng)大頁(yè)。
在ZGC中不同對(duì)象的大小會(huì)使用不同的頁(yè)面類型。下面是ZGC頁(yè)面大小,對(duì)象大小和對(duì)象對(duì)齊數(shù)據(jù):

image.png

其中MinObjectAlignmentInBytes的缺省值8,它由參數(shù)ObjectAlignmentInBytes控制,大小在[8~256]之間,且為2的冪次。

如何開(kāi)啟大頁(yè)面
TBD

ZGC如何管理虛擬內(nèi)存

根據(jù)上面的介紹,ZGC的內(nèi)存劃分幾個(gè)不同的區(qū)域,其中Mark0,Mark1,Remapped這三個(gè)區(qū)的地址關(guān)聯(lián)同一物理地址。而對(duì)于0~4TB區(qū)域的管理,ZGC為了分配效率和垃圾回收效率,原則是:小對(duì)象分配在小頁(yè)面,中等對(duì)象分配在中頁(yè)面,大對(duì)象分配在大頁(yè)面;小頁(yè)分配在虛擬內(nèi)存的頭部、并且優(yōu)先回收,中頁(yè)和大頁(yè)從虛擬內(nèi)存的尾部、且盡量不回收。為什么這么設(shè)計(jì)?原因很簡(jiǎn)單大對(duì)象在垃圾回收時(shí)成本非常高,特別是大對(duì)象的移動(dòng)非常耗時(shí),所以盡量不要移動(dòng)大對(duì)象。

NUMA

過(guò)去,x86系統(tǒng)中的所有內(nèi)存都可以通過(guò)CPU進(jìn)行同等訪問(wèn)。無(wú)論任何CPU執(zhí)行操作,訪問(wèn)時(shí)間都相等,這也被稱為“統(tǒng)一內(nèi)存訪問(wèn)”(UMA,Uniform Memory Access)。
最近使用的x86處理器已經(jīng)不再采取這一行為。在非統(tǒng)一內(nèi)存訪問(wèn)(NUMA,Non-Uniform Memory Access)中,系統(tǒng)內(nèi)存被劃分到NUMA節(jié)點(diǎn)(node),并且與socket 相對(duì)應(yīng),或與特定某一組與本地系統(tǒng)內(nèi)存子集具有相同訪問(wèn)延遲的CPU相對(duì)應(yīng)。

源碼解析

在通常情況下,我們會(huì)設(shè)置推空間的最大值和最小值,在ZGC中我們?nèi)匀豢梢酝ㄟ^(guò)xmx設(shè)置最大堆空間,xms設(shè)置最小堆空間。當(dāng)然如果我們沒(méi)有設(shè)置,JVM會(huì)啟發(fā)式推斷設(shè)置多少的堆空間合適。

內(nèi)存管理器ZHeap

ZGC中堆空間的初始化在ZCollectionHeap中,這個(gè)類是ZGC的入口類,它override了一些關(guān)鍵的函數(shù),比如:
對(duì)象分配相關(guān)的:
ZCollectedHeap::allocate_new_tlab,快速分配,。TLAB塊分配,我們知道JVM中使用一種撞針?lè)峙浞?,即為每個(gè)線程分配一個(gè)TLAB塊,然后在分配小對(duì)象時(shí)優(yōu)先在TLAB中分配,就可以做到讓多個(gè)線程無(wú)沖突的分配,加快了分配的速度。
ZCollectedHeap::mem_allocate,慢速分配。
當(dāng)TLAB中不能分配對(duì)象時(shí)會(huì)調(diào)用這個(gè)函數(shù)分配內(nèi)存。
在ZGC中做法有些不同,快速分配和慢速分配實(shí)際上調(diào)用的同樣的代碼。那么ZGC中不需要快速分配了嗎?還是說(shuō)把慢速分配變成了快速分配一樣的?ZGC中實(shí)現(xiàn)了另外一套對(duì)象管理的方法,在這套方法中還是有快速分配和慢速分配的。
垃圾回收相關(guān):
ZCollectedHeap::collect,垃圾回收。實(shí)際上ZGC是主動(dòng)式垃圾回收,這個(gè)接口只有在顯示調(diào)用垃圾回收才會(huì)用到。

另外在這個(gè)類中還有一個(gè)很關(guān)鍵的工作就是初始化ZHeap對(duì)象。它是ZGC內(nèi)存管理者。它里面字段有:

ZWorkers _workers;并發(fā)、并行工作線程
ZObjectAllocator _object_allocator;對(duì)象分配器
ZPageAllocator _page_allocator;頁(yè)面分配器
ZPageTable _pagetable;頁(yè)表
ZMark _mark;標(biāo)記管理器
ZReferenceProcessor _reference_processor; 引用處理
ZWeakRootsProcessor _weak_roots_processor; 弱根處理
ZRelocate _relocate;轉(zhuǎn)移管理器
ZRelocationSet _relocation_set;轉(zhuǎn)移集合
ZServiceability _serviceability;

初始化在構(gòu)造函數(shù)中。


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());
}

對(duì)象分配

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;
}
對(duì)象分配管理器
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

頁(yè)面管理器ZPageAllocator

頁(yè)面管理是ZGC中關(guān)于內(nèi)存管理最關(guān)鍵的模塊。它的主要字段有:
ZVirtualMemoryManager _virtual; 虛擬地址管理器
ZPhysicalMemoryManager _physical;物理地址管理器
ZPageCache _cache;頁(yè)面緩存
const size_t _max_reserve;內(nèi)存最大保留數(shù)
ZPreMappedMemory _pre_mapped;頁(yè)面預(yù)分配映射
ZList<ZPageAllocRequest> _queue;分隊(duì)列
ZList<ZPage> _detached;

初始化在構(gòu)造函數(shù)中。

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() {}

我們重點(diǎn)關(guān)注虛擬內(nèi)存管理,物理內(nèi)存管理。虛擬地址管理器申請(qǐng)[0, 4TB)作為保留內(nèi)存。物理內(nèi)存管理器僅僅是把GC使用的最大的內(nèi)存Xmx傳遞進(jìn)來(lái),用于控制物理內(nèi)存分配。

虛擬地址管理器
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);
}
物理內(nèi)存管理
ZPhysicalMemory::ZPhysicalMemory(size_t size) :
    _nsegments(0),
    _segments(NULL) {
  add_segment(ZPhysicalMemorySegment(0, size));
}

void ZPhysicalMemory::add_segment(ZPhysicalMemorySegment segment) {
  // 物理內(nèi)存比較簡(jiǎn)單,只支持線性分配。
  // 在添加segment時(shí)當(dāng)發(fā)現(xiàn)要添加的segment和最后一個(gè)segment可以合并時(shí)
  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;
    }
  }

  // 通常只有第一次初始化時(shí)需要分配資源,后續(xù)的物理內(nèi)存分配都是走上面的分支
  // 所以實(shí)際上ZGC的物理內(nèi)存只有一個(gè)大的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的物理地址并不是操作系統(tǒng)中的物理地址,從概念上它更實(shí)際上類似虛擬地址,它是為了管理mutator物理地址的使用,怎么理解這句話呢?ZGC中內(nèi)存空間的分配是以page為粒度(實(shí)際上是以最小page 2MB為粒度)。ZGC為了減少頻繁的請(qǐng)求/釋放物理內(nèi)存,所以設(shè)計(jì)了物理內(nèi)存,物理內(nèi)存實(shí)際上僅僅記錄的是地址的使用情況。

image.png

ZGC中物理內(nèi)存管理的基本單位是segment,它包含start和end。每一個(gè)segment都是2MB。所以可能會(huì)存在一個(gè)超大的對(duì)象10MB,則它需要5個(gè)segment。在ZGC和操作系統(tǒng)的交互時(shí),將分成5次向操作系統(tǒng)申請(qǐng)內(nèi)存,所以可以把一個(gè)大對(duì)象分配在操作系統(tǒng)層面不連續(xù)的物理空間中。但是在物理內(nèi)存管理中,在把這些segment進(jìn)行合并。所以在實(shí)際中很有可能只有一個(gè)segment。
但是ZGC管理的時(shí)候以Page為單位,這時(shí)就會(huì)存在一個(gè)page,包含了5個(gè)segments共同組成這個(gè)對(duì)象。
segment是ZGC向操作系統(tǒng)請(qǐng)求內(nèi)存的基本單位;Page是對(duì)象內(nèi)存管理的單位。

內(nèi)存預(yù)映射管理
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;
}
頁(yè)面分配管理器申請(qǐng)新的頁(yè)面

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

優(yōu)先從cache中分配,當(dāng)page可以回收時(shí)加入到cache中
從預(yù)分配的內(nèi)存中分配,預(yù)分配Premap是在ZGC啟動(dòng)時(shí)按照最小heap的需求分配的內(nèi)存,如果設(shè)置了xms,一般就是設(shè)置的值
如果以上兩種情況都無(wú)法成功分配,則通過(guò)create_page向ZGC內(nèi)存管理器申請(qǐng)新的內(nèi)存page,當(dāng)然在申請(qǐng)的時(shí)候不能超過(guò)堆的最大可用值,在create——page申請(qǐng)新的page之后,會(huì)真正的向操作系統(tǒng)申請(qǐng)物理內(nèi)存,此時(shí)會(huì)調(diào)用mmap完成內(nèi)存映射。

在進(jìn)入alloc_page之前會(huì)更加內(nèi)存的控制策略來(lái)設(shè)置分配是阻塞還是非阻塞。當(dāng)設(shè)置參數(shù)ZStallOnOutOfMemory為false時(shí)進(jìn)行阻塞分配,缺省值為ture,即非阻塞分配。兩者的區(qū)別在于阻塞分配一定會(huì)等著GC完成內(nèi)存的分配直到成功,而非阻塞分配則是在內(nèi)存不足時(shí)GC無(wú)法分配內(nèi)存拋出OutOfMemoryError異常。
在從操作系統(tǒng)中申請(qǐng)內(nèi)存的時(shí)候需要加鎖,因?yàn)橹挥幸粋€(gè)heap。

page和它的狀態(tài)介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 又是一年秋招季,哎呀媽呀我被虐的慘來(lái)~這不,前幾陣失蹤沒(méi)更新博客,其實(shí)是我偷偷把時(shí)間用在復(fù)習(xí)課本了(霧 堅(jiān)持在社區(qū)...
    tengshe789閱讀 2,157評(píng)論 0 8
  • 在linux下,使用top,free等命令查看系統(tǒng)或者進(jìn)程的內(nèi)存使用情況時(shí),經(jīng)??吹絙uff/cache meme...
    analanxingde閱讀 766評(píng)論 0 2
  • 操作系統(tǒng)對(duì)內(nèi)存的管理 沒(méi)有內(nèi)存抽象的年代 在早些的操作系統(tǒng)中,并沒(méi)有引入內(nèi)存抽象的概念。程序直接訪問(wèn)和操作的都是物...
    Mr槑閱讀 16,979評(píng)論 3 24
  • 前言 內(nèi)存管理一向是所有操作系統(tǒng)書(shū)籍不惜筆墨重點(diǎn)討論的內(nèi)容,無(wú)論市面上或是網(wǎng)上都充斥著大量涉及內(nèi)存管理的教材和資料...
    木有sky閱讀 972評(píng)論 0 1
  • 我見(jiàn)過(guò)清澈的詩(shī)句 清澈的回憶 清澈的目光 相遇 沒(méi)見(jiàn)過(guò)清澈的人 偶爾有一雙眼睛 從人群深處 閃爍出來(lái) 人們說(shuō) 那樣...
    段童閱讀 259評(píng)論 0 2

友情鏈接更多精彩內(nèi)容