https://zhuanlan.zhihu.com/p/533305428
我最近花了很多时间在 JVM 的内存预留代码上。它开始是因为我们得到了外部贡献,以支持在 Linux 上使用多个大小的 large page。为了以一种好的方式做到这一点,必须首先重构一些其他的东西。在沿着 内存通道 进行这次旅行时,我意识到对 JVM 使用的 large pages 做一个简短的总结可能是一篇非常有趣的文章。
在我们开始讨论 JVM 如何使用它们之前,让我们先简要介绍一下什么是 large pages(大页)。
Large pages 或者叫 huge pages, 是一种减少处理器 TLB 缓存压力的技术。这些缓存用于加快将虚拟地址转换为物理内存地址的时间。大多数体系结构支持多种 page 大小,通常基页大小为 4 KB。对于使用大量内存的应用程序,例如大型 Java 堆,使用更大的 page 粒度映射内存以增加 TLB 中的命中率是有意义的。在 x86-64 上,2 MB 和 1 GB page 可用于此目的,对于内存密集型工作负载,这可能会产生非常大的影响。
Large pages Enabled
在上图中,我们可以看到在使用和不使用大页面时运行几个 SPECjbb® 1基准测试的差异。配置上的唯一区别是高性能 JVM 启用了 large pages。结果非常令人印象深刻,对于许多 Java 工作负载来说,启用 large pages 是一个巨大的胜利。
为 Java 启用 large pages 的通用开关是-XX:+UseLargePages,但要利用 large pages,还需要正确配置操作系统。让我们看看如何在 Linux 和 Windows 中配置。
在 Linux 上,JVM 可以通过两种不同的方式使用 large pages:Transparent Huge Pages 和 HugeTLB pages。它们在配置方式上有所不同,但在性能特征上也略有不同。
Transparent Huge Pages,简称 THP,是一种在 Linux 中简化 large pages 使用和启用的方法。启用后,Linux 内核将尝试使用 large pages 来保留足够大且有资格使用 THP。可以在三个不同级别配置 THP 支持:
配置存储在
/sys/kernel/mm/transparent_hugepage/enabled 可以像这样轻松更改:
$ echo "madvise" > /sys/kernel/mm/transparent_hugepage/enabled
JVM 支持在 madvise mode 中配置时使用 THP ,但需要使用 -XX:+UseTransparentHugePages。完成此操作后,Java 堆以及其他内部 JVM 数据结构将由 transparent huge pages 支持。
为了使内核能够满足使用 transparent huge pages 的请求,需要有足够的连续物理内存可用。如果没有内核将尝试对内存进行碎片整理以满足请求。碎片整理可以通过几种不同的方式进行配置,当前策略存储在
/sys/kernel/mm/transparent_hugepage/defrag. 有关此配置和其他配置的更多详细信息,请参阅 kernel 内核文档。
这种类型的 large pages 由操作系统预先分配并消耗用于支持它们的物理内存。应用程序可以使用mmap()标志从这个池中保留 page MAP_HUGETLB。这是在 Linux 上为 JVM 使用 large pages 的默认方式,可以通过设置-XX:+UseLargePages或特定标记启用-XX:+UseHugeTLBFS。
当 JVM 使用这种类型的 large pages 时,它会在前面提交由 large pages 支持的整个内存范围。这是确保没有其他预留耗尽操作系统分配的 large pages 池所必需的。这也意味着需要预先分配足够的 large pages 以在保留时支持整个内存范围,否则 JVM 将回退到使用正常 page。
要配置这种类型的 large pages,首先检查可用的 page 大小:
$ ls /sys/kernel/mm/hugepages/
hugepages-1048576kB hugepages-2048kB
然后根据你的需求配置 page 大小,如下所示:
$ echo 2500 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
这会尝试分配 2500 个 2 MB 的 pages。应该始终读取实际存储在 nr_hugepages 中的值,以确保内核能够分配请求的数量。
默认情况下,JVM 将在尝试保留 large pages 时使用系统环境默认 large page 大小,你可以通过下面的方式查看系统默认的 large page 大小:
$ cat /proc/meminfo | grep Hugepagesize
Hugepagesize: 2048 kB
如果你想使用不同的 large page 大小,可以通过设置 JVM LargePageSizeInBytes 标记来完成 。例如:使用 1 GB 的页面-XX:LargePageSizeInBytes=1g。
更多关于 HugeTLB pages 的信息也可以查看 kernel 内核文档。
两种方法各有利弊,选择哪一种取决于多个方面。THP 更易于设置和使用,但在使用 HugeTLB 页面时你有更多控制权。如果延迟是你最关心的问题,那么你可能应该使用 HugeTLB 页面,因为你永远不会等待操作系统释放足够的连续内存。作为替代方案,你可以将 defrag THP 选项配置为在没有 large pages 可用时不停止,但这可能会带来吞吐量成本。如果内存占用是一个问题,THP 是避免必须预先提交整个 Java 堆得更好选择。
使用哪种类型的 large pages 取决于应用程序和环境,但在很多情况下,使用任何类型的 large pages 都会对性能产生积极影响。
在 Windows 上,配置步骤要容易一些,至少在较新的版本上是这样。运行要使用 large pages 的进程的用户需要具有 锁定内存页 权限。在 Windows 10 上,这是通过以下方式完成的:
一旦授予此权限,JVM 将能够使用 large pages(如果使用-XX:+UseLargePages. Windows 上的 JVM large pages 实现与 Linux 上 HugeTLB pages 非常相似。由 large pages 支持的整个预留是预先提交的,以确保我们以后不会出现任何故障。
直到最近,有一个 bug 阻止 G1(默认 GC)在 Windows 上为大于 4 GB 的堆使用 large pages。现在已修复此问题,并且继续使用G1运行大型Minecraft 服务器应该能够通过启用 large pages ** 获得不错的提升**。
一旦你的环境配置正确并且你已经在运行时启用 Java large pages,最好验证 JVM 是否真的使用了 large pages。你可以使用你最喜欢的操作系统工具来检查这一点,但 JVM 也有一些日志记录选项可以帮助解决这个问题。要查看一些基本的 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 上运行的,我们可以看到启用了 Large Page 支持。Explicit 意味着使用了 HugeTLB 页面。如果使用-XX:+UseTransparentHugePages日志行运行将如下所示:
[0.030s][info][gc,init] Large Page Support: Enabled (Transparent)
以上仅显示是否启用了 large pages,如果你想了解有关使用 large pages JVM 的更多详细信息,你可以启用-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
这是非常详细的信息,它是验证 JVM 的哪些部分由 large pages 支持的好方法。上面的输出是使用 JDK 16 生成的,它有一个 bug 导致 CodeHeap 页面大小不正确,它们也由 large pages 支持。