内存管理

原生镜像在执行时,不运行在Java HotSpot VM上,而是运行在GraalVM提供的运行时系统上。该运行时系统包含所有必要的组件,其中之一是内存管理。

原生镜像在运行时分配的Java对象位于称为“Java堆”的区域。Java堆在原生镜像启动时创建,并且在原生镜像运行时可能会增大或减小。当堆满时,会触发垃圾回收以回收不再使用的对象的内存。

为了管理Java堆,Native Image提供了不同的垃圾回收器 (GC) 实现。

  • Serial GC 是GraalVM Native Image中的默认GC。它针对低内存占用和小型Java堆进行了优化。
  • G1 GC 是一种多线程GC,旨在减少“stop-the-world”暂停,从而提高延迟,同时实现高吞吐量。要启用它,请将选项 —gc=G1 传递给 native-image 构建器。目前,G1垃圾回收器可在Linux AMD64和AArch64架构上的Native Image中使用。(GraalVM社区版中不可用。)
  • Epsilon GC(GraalVM 21.2或更高版本提供)是一个无操作的垃圾回收器,不执行任何垃圾回收,因此从不释放任何已分配的内存。该GC的主要用例是非常短时运行且只分配少量内存的应用程序。要在镜像构建时启用Epsilon GC,请指定选项 —gc=epsilon

性能考量 #

垃圾回收的主要衡量指标是吞吐量、延迟和内存占用。

  • 吞吐量 是在长时间内不用于垃圾回收的总时间的百分比。
  • 延迟 是应用程序的响应能力。垃圾回收暂停会对响应能力产生负面影响。
  • 内存占用 是进程的工作集,以页和缓存行衡量。

Java堆设置的选择始终是这些指标之间的权衡。例如,非常大的年轻代可能会最大化吞吐量,但这会牺牲内存占用和延迟。通过使用小型年轻代来最大程度地减少年轻代暂停,但这会牺牲吞吐量。

默认情况下,Native Image会自动确定下列Java堆设置的值。具体值可能取决于系统配置和所使用的GC。

  • 最大Java堆大小 定义了整个Java堆大小的上限。如果Java堆已满,并且GC无法为Java对象分配回收足够的内存,则分配将因 OutOfMemoryError 而失败。注意:最大堆大小仅是Java堆的上限,不一定是总消耗内存量的上限,因为Native Image会将某些数据(例如线程栈、即时编译代码(用于Truffle运行时编译)和内部数据结构)放置在与Java堆分离的内存中。
  • 最小Java堆大小 定义了GC始终可假定为Java堆保留的内存量,无论实际使用了多少内存。
  • 年轻代大小 决定了可以在不触发垃圾回收的情况下分配的Java内存量。

Serial垃圾回收器 #

Serial GC 针对低内存占用和小型Java堆进行了优化。如果未指定其他GC,Serial GC将作为GraalVM上的默认GC隐式使用。也可以通过将选项 —gc=serial 传递给原生镜像构建器来显式启用Serial GC。

# Build a native image that uses the serial GC with default settings
native-image --gc=serial HelloWorld

概述 #

从核心来看,Serial GC是一个简单的(非并行、非并发)停止复制GC。它将Java堆分为年轻代和老年代。每个代都由一组大小相等的块组成,每个块都是一个连续的虚拟内存范围。这些块是GC内部用于内存分配和内存回收的单元。

年轻代包含最近创建的对象,并分为伊甸区(eden)幸存区(survivor)。新对象在伊甸区分配,当该区域满时,会触发一次年轻代回收。伊甸区中存活的对象将被移至幸存区,幸存区中存活的对象会留在该区域,直到它们达到一定年龄(即经过一定次数的回收),此时它们被移至老年代。当老年代变满时,会触发一次完全回收,回收年轻代和老年代中未使用对象的空间。通常,年轻代回收比完全回收快得多,但进行完全回收对于保持低内存占用至关重要。默认情况下,Serial GC会尝试为各代找到一个能提供良好吞吐量的大小,但当进一步增加大小导致收益递减时,则不再增加。它还尝试在年轻代回收和完全回收所花费的时间之间保持一个比例,以保持较小的内存占用。

如果未指定最大Java堆大小,使用Serial GC的原生镜像会将其最大Java堆大小设置为物理内存大小的80%。例如,在具有4GB RAM的机器上,最大Java堆大小将设置为3.2GB。如果同一镜像在具有32GB RAM的机器上执行,最大Java堆大小将设置为25.6GB。请注意,这只是最大值。根据应用程序的不同,实际使用的Java堆内存量可能要低得多。要覆盖此默认行为,可以为 -XX:MaximumHeapSizePercent 指定一个值,或显式设置最大Java堆大小

请注意,GraalVM 21.3及更早版本使用Serial GC的不同默认配置,该配置没有幸存区,年轻代限制为256 MB,以及一种平衡年轻代回收和老年代回收时间的默认回收策略。此配置可以通过以下方式启用: —H:InitialCollectionPolicy=BySpaceAndTime

请注意,GC在执行垃圾回收时需要一些额外的内存(最坏情况下是最大堆大小的2倍,通常会显著减少)。因此,驻留集大小(RSS)在垃圾回收期间可能会暂时增加,这在任何有内存限制的环境(例如容器)中都可能成为问题。

性能调优 #

为了调优GC性能和内存占用,可以使用以下选项:

  • —XX:MaximumHeapSizePercent - 如果未另行指定最大Java堆大小,则用作最大Java堆大小的物理内存大小百分比。
  • —XX:MaximumYoungGenerationSizePercent - 年轻代的最大大小,以最大Java堆大小的百分比表示。
  • —XX:±CollectYoungGenerationSeparately (GraalVM 21.0起) - 确定完全GC是单独回收年轻代还是与老年代一起回收。如果启用,这可能会减少完全GC期间的内存占用。但是,完全GC可能需要更多时间。
  • —XX:MaxHeapFree (GraalVM 21.3起) - 回收后仍保留用于分配的空闲内存块的最大总大小(以字节为单位),因此不会返回给操作系统。
  • —H:AlignedHeapChunkSize (只能在镜像构建时指定) - 堆块的大小,以字节为单位。
  • —H:MaxSurvivorSpaces (GraalVM 21.1起,只能在镜像构建时指定) - 用于年轻代的幸存区数量,即对象晋升到老年代的最大年龄。如果值为0,则在年轻代回收中幸存的对象将直接晋升到老年代。
  • —H:LargeArrayThreshold (只能在镜像构建时指定) - 数组在其自己的堆块中分配的最小大小。被视为大型的数组分配成本更高,但GC从不复制它们,这可以减少GC开销。
# Build and execute a native image that uses a maximum heap size of 25% of the physical memory
native-image --gc=serial -R:MaximumHeapSizePercent=25 HelloWorld
./helloworld

# Execute the native image from above but increase the maximum heap size to 75% of the physical memory
./helloworld -XX:MaximumHeapSizePercent=75

以下选项仅在使用 —H:InitialCollectionPolicy=BySpaceAndTime 时可用:

  • —XX:PercentTimeInIncrementalCollection - 确定GC应花费多少时间进行年轻代回收。默认值为50时,GC会尝试平衡年轻代回收和完全回收所花费的时间。增加此值将减少完全GC的数量,这可以提高性能但可能会恶化内存占用。减少此值将增加完全GC的数量,这可以改善内存占用但可能会降低性能。

G1垃圾回收器 #

Oracle GraalVM还提供了Garbage-First (G1) 垃圾回收器,它基于Java HotSpot VM中的G1 GC。目前,G1垃圾回收器可在Linux AMD64和AArch64架构上的Native Image中使用。(GraalVM社区版中不可用。)

要启用它,请将选项 —gc=G1 传递给 native-image 构建器。

# Build a native image that uses the G1 GC with default settings
native-image --gc=G1 HelloWorld

注意:在GraalVM 20.0、20.1和20.2中,G1 GC被称为低延迟GC,可以通过实验性选项 —H:+UseLowLatencyGC 启用。

概述 #

G1是一种分代、增量、并行、大部分并发、停顿世界(stop-the-world)和疏散式GC。它旨在提供延迟和吞吐量之间的最佳平衡。

某些操作始终在“stop-the-world”暂停中执行以提高吞吐量。其他在应用程序停止时会花费更多时间的操作,例如全局标记等全堆操作,则与应用程序并行且并发执行。G1 GC尝试在较长时间内以高概率满足设定的暂停时间目标。但是,对于给定的暂停时间并没有绝对的确定性。

G1将堆划分为一组大小相等的堆区域,每个区域都是一个连续的虚拟内存范围。区域是GC内部用于内存分配和内存回收的单元。在任何给定时间,这些区域中的每一个都可以是空的,或者被分配给特定的代。

如果未指定最大Java堆大小,使用G1 GC的原生镜像会将其最大Java堆大小设置为物理内存大小的25%。例如,在具有4GB RAM的机器上,最大Java堆大小将设置为1GB。如果同一镜像在具有32GB RAM的机器上执行,最大Java堆大小将设置为8GB。要覆盖此默认行为,可以为 -XX:MaxRAMPercentage 指定一个值,或显式设置最大Java堆大小

性能调优 #

G1 GC是一个自适应垃圾回收器,其默认设置使其无需修改即可高效工作。但是,它可以根据特定应用程序的性能需求进行调优。以下是在进行性能调优时可以指定的一小部分选项:

  • —H:G1HeapRegionSize (只能在镜像构建时指定) - G1区域的大小。
  • —XX:MaxRAMPercentage - 如果未另行指定最大堆大小,则用作最大堆大小的物理内存大小百分比。
  • —XX:MaxGCPauseMillis - 最大暂停时间的目标。
  • —XX:ParallelGCThreads - 垃圾回收暂停期间用于并行工作的最大线程数。
  • —XX:ConcGCThreads - 用于并发工作的最大线程数。
  • —XX:InitiatingHeapOccupancyPercent - 触发标记周期的Java堆占用阈值。
  • —XX:G1HeapWastePercent - 集合集候选区中允许的未回收空间。如果集合集候选区中的空闲空间低于此值,G1将停止空间回收阶段。
# Build and execute a native image that uses the G1 GC with a region size of 2MB and a maximum pause time goal of 100ms
native-image --gc=G1 -H:G1HeapRegionSize=2m -R:MaxGCPauseMillis=100 HelloWorld
./helloworld

# Execute the native image from above and override the maximum pause time goal
./helloworld -XX:MaxGCPauseMillis=50

内存管理选项 #

本节描述了最重要的内存管理命令行选项,它们独立于所使用的GC。对于所有数值,可以使用后缀 kmg 进行缩放。可以使用 native-image --expert-options-all 列出更多原生镜像构建器选项。

Java堆大小 #

执行原生镜像时,将根据系统配置和所使用的GC自动确定合适的Java堆设置。要覆盖此自动机制并显式设置运行时堆大小,可以使用以下命令行选项:

  • —Xmx - 最大堆大小(字节)
  • —Xms - 最小堆大小(字节)
  • —Xmn - 年轻代大小(字节)

也可以在镜像构建时预配置默认堆设置。指定的值随后将用作运行时默认值。

  • —R:MaxHeapSize (GraalVM 20.0起) - 最大堆大小(字节)
  • —R:MinHeapSize (GraalVM 20.0起) - 最小堆大小(字节)
  • —R:MaxNewSize (GraalVM 20.0起) - 年轻代大小(字节)
# Build a native image with the default heap settings and override the heap settings at run time
native-image HelloWorld
./helloworld -Xms2m -Xmx10m -Xmn1m

# Build a native image and "bake" heap settings into the image. The specified values will be used at run time
native-image -R:MinHeapSize=2m -R:MaxHeapSize=10m -R:MaxNewSize=1m HelloWorld
./helloworld

压缩引用 #

Oracle GraalVM支持对Java对象使用压缩引用,这些引用使用32位而不是64位。压缩引用默认启用,并且对内存占用有很大影响。但是,它们将最大Java堆大小限制为32 GB内存。如果需要超过32 GB,则需要禁用压缩引用。

  • —H:±UseCompressedReferences (只能在镜像构建时指定) - 确定是否使用32位而不是64位引用来指向Java对象。

本机内存 #

Native Image还可能分配与Java堆分离的内存。一个常见的用例是直接引用本机内存的 java.nio.DirectByteBuffer

  • —XX:MaxDirectMemorySize - 直接缓冲区分配的最大大小。

打印垃圾回收信息 #

执行原生镜像时,可以使用以下选项打印一些垃圾回收信息。具体打印哪些数据取决于所使用的GC。

  • —XX:+PrintGC - 打印每次垃圾回收的基本信息
  • —XX:+VerboseGC - 可添加此选项以打印更多垃圾回收详情
# Execute a native image and print basic garbage collection information
./helloworld -XX:+PrintGC

# Execute a native image and print detailed garbage collection information
./helloworld -XX:+PrintGC -XX:+VerboseGC

延伸阅读 #

联系我们