我最近花了很多時(shí)間在 JVM 的內(nèi)存預(yù)留代碼上。它開(kāi)始是因?yàn)槲覀兊玫搅送獠控暙I(xiàn),以支持在 Linux 上使用多個(gè)大小的 large page。為了以一種好的方式做到這一點(diǎn),必須首先重構(gòu)一些其他的東西。在沿著 內(nèi)存通道 進(jìn)行這次旅行時(shí),我意識(shí)到對(duì) JVM 使用的 large pages 做一個(gè)簡(jiǎn)短的總結(jié)可能是一篇非常有趣的閱讀。
在我們開(kāi)始討論 JVM 如何使用它們之前,讓我們先簡(jiǎn)要介紹一下什么是 large pages(大頁(yè))。
Large pages 或者叫 huge pages, 是一種減少處理器 TLB 緩存壓力的技術(shù)。這些緩存用于加快將虛擬地址轉(zhuǎn)換為物理內(nèi)存地址的時(shí)間。大多數(shù)體系結(jié)構(gòu)支持多種 page 大小,通常基頁(yè)大小為 4 KB。對(duì)于使用大量?jī)?nèi)存的應(yīng)用程序,例如大型 Java 堆,使用更大的 page 粒度映射內(nèi)存以增加 TLB 中的命中率是有意義的。在 x86-64 上,2 MB 和 1 GB page 可用于此目的,對(duì)于內(nèi)存密集型工作負(fù)載,這可能會(huì)產(chǎn)生非常大的影響。
Large pages Enabled
在上圖中,我們可以看到在使用和不使用大頁(yè)面時(shí)運(yùn)行幾個(gè) SPECjbb? 基準(zhǔn)測(cè)試的差異。配置上的唯一區(qū)別是高性能 JVM 啟用了 large pages。結(jié)果非常令人印象深刻,對(duì)于許多 Java 工作負(fù)載來(lái)說(shuō),啟用 large pages 是一個(gè)巨大的勝利。
為 Java 啟用 large pages 的通用開(kāi)關(guān)是 -XX:+UseLargePages,但要利用 large pages,還需要正確配置操作系統(tǒng)。讓我們看看如何在 Linux 和 Windows 中配置。
在 Linux 上,JVM 可以通過(guò)兩種不同的方式使用 large pages:Transparent Huge Pages 和 HugeTLB pages。它們?cè)谂渲梅绞缴嫌兴煌?strong>性能特征上也略有不同。
Transparent Huge Pages,簡(jiǎn)稱 THP,是一種在 Linux 中簡(jiǎn)化 large pages 使用和啟用的方法。啟用后,Linux 內(nèi)核將嘗試使用 large pages 來(lái)保留足夠大且有資格使用 THP。可以在三個(gè)不同級(jí)別配置 THP 支持:
配置存儲(chǔ)在 /sys/kernel/mm/transparent_hugepage/enabled 可以像這樣輕松更改:
$ echo "madvise" > /sys/kernel/mm/transparent_hugepage/enabled
JVM 支持在 madvise mode 中配置時(shí)使用 THP ,但需要使用 -XX:+UseTransparentHugePages。完成此操作后,Java 堆以及其他內(nèi)部 JVM 數(shù)據(jù)結(jié)構(gòu)將由 transparent huge pages 支持。
為了使內(nèi)核能夠滿足使用 transparent huge pages 的請(qǐng)求,需要有足夠的連續(xù)物理內(nèi)存可用。如果沒(méi)有內(nèi)核將嘗試對(duì)內(nèi)存進(jìn)行碎片整理以滿足請(qǐng)求。碎片整理可以通過(guò)幾種不同的方式進(jìn)行配置,當(dāng)前策略存儲(chǔ)在 /sys/kernel/mm/transparent_hugepage/defrag。有關(guān)此配置和其他配置的更多詳細(xì)信息,請(qǐng)參閱 kernel 內(nèi)核文檔。
這種類型的 large pages 由操作系統(tǒng)預(yù)先分配并消耗用于支持它們的物理內(nèi)存。應(yīng)用程序可以使用 mmap() 標(biāo)志從這個(gè)池中保留 page MAP_HUGETLB。這是在 Linux 上為 JVM 使用 large pages 的默認(rèn)方式,可以通過(guò)設(shè)置 -XX:+UseLargePages 或特定標(biāo)記啟用 -XX:+UseHugeTLBFS。
當(dāng) JVM 使用這種類型的 large pages 時(shí),它會(huì)在前面提交由 large pages 支持的整個(gè)內(nèi)存范圍。這是確保沒(méi)有其他預(yù)留耗盡操作系統(tǒng)分配的 large pages 池所必需的。這也意味著需要預(yù)先分配足夠的 large pages 以在保留時(shí)支持整個(gè)內(nèi)存范圍,否則 JVM 將回退到使用正常 page。
要配置這種類型的 large pages,首先檢查可用的 page 大小:
$ ls /sys/kernel/mm/hugepages/
hugepages-1048576kB hugepages-2048kB
然后根據(jù)你的需求配置 page 大小,如下所示:
$ echo 2500 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
這會(huì)嘗試分配 2500 個(gè) 2 MB 的 pages。應(yīng)該始終讀取實(shí)際存儲(chǔ)在 nr_hugepages 中的值,以確保內(nèi)核能夠分配請(qǐng)求的數(shù)量。
默認(rèn)情況下,JVM 將在嘗試保留 large pages 時(shí)使用系統(tǒng)環(huán)境默認(rèn) large page 大小,你可以通過(guò)下面的方式查看系統(tǒng)默認(rèn)的 large page 大小:
$ cat /proc/meminfo | grep Hugepagesize
Hugepagesize: 2048 kB
如果想使用不同的 large page 大小,可以通過(guò)設(shè)置 JVM LargePageSizeInBytes 標(biāo)記來(lái)完成 。例如:使用 1 GB 的頁(yè)面 -XX:LargePageSizeInBytes=1g。
更多關(guān)于 HugeTLB pages 的信息也可以查看 kernel 內(nèi)核文檔。
兩種方法各有利弊,選擇哪一種取決于多個(gè)方面。THP 更易于設(shè)置和使用,但在使用 HugeTLB 頁(yè)面時(shí)你有更多控制權(quán)。如果延遲是你最關(guān)心的問(wèn)題,那么你可能應(yīng)該使用 HugeTLB 頁(yè)面,因?yàn)槟阌肋h(yuǎn)不會(huì)等待操作系統(tǒng)釋放足夠的連續(xù)內(nèi)存。作為替代方案,你可以將 defrag THP 選項(xiàng)配置為在沒(méi)有 large pages 可用時(shí)不停止,但這可能會(huì)帶來(lái)吞吐量成本。如果內(nèi)存占用是一個(gè)問(wèn)題,THP 是避免必須預(yù)先提交整個(gè) Java 堆的更好選擇。
使用哪種類型的 large pages 取決于應(yīng)用程序和環(huán)境,但在很多情況下,使用任何類型的 large pages 都會(huì)對(duì)性能產(chǎn)生積極影響。
在 Windows 上,配置步驟要容易一些,至少在較新的版本上是這樣。運(yùn)行要使用 large pages 的進(jìn)程的用戶需要具有 鎖定內(nèi)存頁(yè) 權(quán)限。在 Windows 10 上,這是通過(guò)以下方式完成的:
一旦授予此權(quán)限,JVM 將能夠使用 large pages(如果使用 -XX:+UseLargePages。Windows 上的 JVM large pages 實(shí)現(xiàn)與 Linux 上 HugeTLB pages 非常相似。由 large pages 支持的整個(gè)預(yù)留是預(yù)先提交的,以確保我們以后不會(huì)出現(xiàn)任何故障。
直到最近,有一個(gè) bug 阻止 G1(默認(rèn) GC)在 Windows 上為大于 4 GB 的堆使用 large pages。現(xiàn)在已修復(fù)此問(wèn)題,并且繼續(xù)使用G1運(yùn)行大型Minecraft 服務(wù)器應(yīng)該能夠通過(guò)啟用 large pages 獲得不錯(cuò)的提升。
一旦你的環(huán)境配置正確并且你已經(jīng)在運(yùn)行時(shí)啟用 Java large pages,最好驗(yàn)證 JVM 是否真的使用了 large pages。你可以使用你最喜歡的操作系統(tǒng)工具來(lái)檢查這一點(diǎn),但 JVM 也有一些日志記錄選項(xiàng)可以幫助解決這個(gè)問(wèn)題。要查看一些基本的 GC 配置,你可以使用 -Xlog:gc+init。使用 G1 你可以看到以下輸出:
> jdk-16/bin/java -Xlog:gc+init -XX:+UseLargePages -Xmx4g -version
[0.029s][info][gc,init] Version: 16+36-2231 (release)
[0.029s][info][gc,init] CPUs: 40 total, 40 available
[0.029s][info][gc,init] Memory: 64040M
[0.029s][info][gc,init] Large Page Support: Enabled (Explicit)
[0.029s][info][gc,init] NUMA Support: Disabled
[0.029s][info][gc,init] Compressed Oops: Enabled (Zero based)
[0.029s][info][gc,init] Heap Region Size: 2M
[0.029s][info][gc,init] Heap Min Capacity: 8M
[0.029s][info][gc,init] Heap Initial Capacity: 1002M
[0.029s][info][gc,init] Heap Max Capacity: 4G
[0.029s][info][gc,init] Pre-touch: Disabled
[0.029s][info][gc,init] Parallel Workers: 28
[0.029s][info][gc,init] Concurrent Workers: 7
[0.029s][info][gc,init] Concurrent Refinement Workers: 28
[0.029s][info][gc,init] Periodic GC: Disabled
這是在 Linux 上運(yùn)行的,我們可以看到啟用了 Large Page 支持。Explicit 意味著使用了 HugeTLB 頁(yè)面。如果使用 -XX:+UseTransparentHugePages 日志行運(yùn)行將如下所示:
[0.030s][info][gc,init] Large Page Support: Enabled (Transparent)
以上僅顯示是否啟用了 large pages,如果你想了解有關(guān)使用 large pages JVM 的更多詳細(xì)信息,你可以啟用 -Xlog:pagesize 并獲得如下輸出:
[0.002s][info][pagesize] CodeHeap 'non-nmethods': min=2496K max=8M base=0x00007fed3d600000 page_size=4K size=8M
[0.002s][info][pagesize] CodeHeap 'profiled nmethods': min=2496K max=116M base=0x00007fed3de00000 page_size=4K size=116M
[0.002s][info][pagesize] CodeHeap 'non-profiled nmethods': min=2496K max=116M base=0x00007fed45200000 page_size=4K size=116M
[0.026s][info][pagesize] Heap: min=8M max=4G base=0x0000000700000000 page_size=2M size=4G
[0.026s][info][pagesize] Block Offset Table: req_size=8M base=0x00007fed3c000000 page_size=2M alignment=2M size=8M
[0.026s][info][pagesize] Card Table: req_size=8M base=0x00007fed3b800000 page_size=2M alignment=2M size=8M
[0.026s][info][pagesize] Card Counts Table: req_size=8M base=0x00007fed3b000000 page_size=2M alignment=2M size=8M
[0.026s][info][pagesize] Prev Bitmap: req_size=64M base=0x00007fed37000000 page_size=2M alignment=2M size=64M
[0.026s][info][pagesize] Next Bitmap: req_size=64M base=0x00007fed33000000 page_size=2M alignment=2M size=64M
這是非常詳細(xì)的信息,它是驗(yàn)證 JVM 的哪些部分由 large pages 支持的好方法。上面的輸出是使用 JDK 16 生成的,它有一個(gè) bug 導(dǎo)致 CodeHeap 頁(yè)面大小不正確,它們也由 large pages 支持。
這篇文章是另外一篇文章 Java17 GC 文章的前置篇,我們已經(jīng)陸續(xù)發(fā)了多篇 Java17 相關(guān)文章:
還有一批文章正在醞釀。另外筆者開(kāi)源的微服務(wù)組件 mica 已經(jīng)適配 java17,mybatis-plus 的 pr 已發(fā)。關(guān)注我,java 學(xué)習(xí)不迷路!!!
作者:盧鈞軼(cenalulu) 本文原文地址:http://cenalulu.github.io/linux/huge-page-on-numa/
在閱讀本文之前,需要讀者至少了解以下基礎(chǔ)知識(shí)
在正式開(kāi)始本文分析前,我們先大概介紹下Huge Page的歷史背景和使用場(chǎng)景。
為什么需要Huge Page 了解CPU Cache大致架構(gòu)的話,一定聽(tīng)過(guò)TLB Cache。Linux系統(tǒng)中,對(duì)程序可見(jiàn)的,可使用的內(nèi)存地址是Virtual Address。每個(gè)程序的內(nèi)存地址都是從0開(kāi)始的。而實(shí)際的數(shù)據(jù)訪問(wèn)是要通過(guò)Physical Address進(jìn)行的。因此,每次內(nèi)存操作,CPU都需要從page table中把Virtual Address翻譯成對(duì)應(yīng)的Physical Address,那么對(duì)于大量?jī)?nèi)存密集型程序來(lái)說(shuō)page table的查找就會(huì)成為程序的瓶頸。所以現(xiàn)代CPU中就出現(xiàn)了TLB(Translation Lookaside Buffer) Cache用于緩存少量熱點(diǎn)內(nèi)存地址的mapping關(guān)系。然而由于制造成本和工藝的限制,響應(yīng)時(shí)間需要控制在CPU Cycle級(jí)別的Cache容量只能存儲(chǔ)幾十個(gè)對(duì)象。那么TLB Cache在應(yīng)對(duì)大量熱點(diǎn)數(shù)據(jù)Virual Address轉(zhuǎn)換的時(shí)候就顯得捉襟見(jiàn)肘了。我們來(lái)算下按照標(biāo)準(zhǔn)的Linux頁(yè)大小(page size) 4K,一個(gè)能緩存64元素的TLB Cache只能涵蓋4K*64=256K的熱點(diǎn)數(shù)據(jù)的內(nèi)存地址,顯然離理想非常遙遠(yuǎn)的。于是Huge Page就產(chǎn)生了。 Tips: 這里不要把Virutal Address和Windows上的虛擬內(nèi)存搞混了。后者是為了應(yīng)對(duì)物理內(nèi)存不足,而將內(nèi)容從內(nèi)存換出到其他設(shè)備的技術(shù)(類似于Linux的SWAP機(jī)制)。
什么是Huge Page 既然改變不了TLB Cache的容量,那么只能從系統(tǒng)層面增加一個(gè)TLB Cache entry所能對(duì)應(yīng)的物理內(nèi)存大小,從而增加TLB Cache所能涵蓋的熱點(diǎn)內(nèi)存數(shù)據(jù)量。假設(shè)我們把Linux Page Size增加到16M,那么同樣一個(gè)容納64個(gè)元素的TLB Cache就能顧及64*16M=1G的內(nèi)存熱點(diǎn)數(shù)據(jù),這樣的大小相較上文的256K就顯得非常適合實(shí)際應(yīng)用了。像這種將Page Size加大的技術(shù)就是Huge Page。
了解了Huge Page的由來(lái)和原理后,我們不難總結(jié)出能從Huge Page受益的程序必然是那些熱點(diǎn)數(shù)據(jù)分散且至少超過(guò)64個(gè)4K Page Size的程序。此外,如果程序的主要運(yùn)行時(shí)間并不是消耗在TLB Cache Miss后的Page Table Lookup上,那么TLB再怎么大,Page Size再怎么增加都是徒勞。在LWN的一篇入門(mén)介紹中就提到了這個(gè)原理,并且給出了比較詳細(xì)的估算方法。簡(jiǎn)單的說(shuō)就是:先通過(guò)oprofile抓取到TLB Miss導(dǎo)致的運(yùn)行時(shí)間占程序總運(yùn)行時(shí)間的多少,來(lái)計(jì)算出Huge Page所能帶來(lái)的預(yù)期性能提升。 簡(jiǎn)單的說(shuō),我們的程序如果熱點(diǎn)數(shù)據(jù)只有256K,并且集中在連續(xù)的內(nèi)存page上,那么一個(gè)64個(gè)entry的TLB Cache就足以應(yīng)付了。說(shuō)道這里,大家可能有個(gè)疑問(wèn)了:既然我們比較難預(yù)測(cè)自己的程序訪問(wèn)邏輯是否能從開(kāi)啟Huge Page中受益。反正Huge Page看上去只改了一個(gè)Page Size,不會(huì)有什么性能損失。那么我們就索性對(duì)所有程序都是用Huge Page好啦。 其實(shí)這樣的想法是完全錯(cuò)誤的!也正是本文想要介紹的一個(gè)主要內(nèi)容,在目前常見(jiàn)的NUMA體系下Huge Page也并非萬(wàn)能鑰匙,使用不當(dāng)甚至?xí)沟贸绦蚧蛘邤?shù)據(jù)庫(kù)性能下降10%。下面我們重點(diǎn)分析。
Large Pages May Be Harmful on NUMA Systems