Last Updated:

JVM 如何使用和分配内存

银狐

这是一个系列的第二篇文章解释了垃圾收集的Java以及如何调整它以获得最佳的Java应用程序的性能。上一篇文章介绍了垃圾收集(包括分代垃圾收集)的阶段和级别,并展示了如何检查应用程序中的垃圾收集行为。本文更深入地介绍了 Java 虚拟机 (JVM) 中的内存使用以及如何控制它。

跟踪 JVM 中的内存使用情况

如果未正确调优,垃圾收集会对 Java 应用程序性能产生负面和不可预测的影响。例如,当全收集事件发生过于频繁时,它们会导致应用服务器上的 CPU 使用率过高,从而导致应用请求处理不佳。

如果垃圾收集发生得太频繁或占用了 CPU 的很大一部分,那么首先要做的是检查您的应用程序是否在不必要地分配内存。过度分配通常是由于内存泄漏。

接下来,在开发环境中使用预期的生产负载测试您的应用程序,以确定最大堆内存使用量。您的生产堆大小应至少比测试的最大值高 25% 到 30%,以便为开销留出空间。

检测内存不足错误

启用该-XX:HeapDumpOnOutOfMemoryError选项生成时,从Java堆分配不能满足堆转储和应用程序失败并OutOfMemoryError。堆转储可以帮助您找到原因。

-XX:HeapDumpOnOutOfMemoryError选项提供有关内存不足错误的关键信息。配置该选项不会对您的环境产生任何性能影响,因此您可以在生产环境中启用它。始终建议启用此选项。

默认情况下,堆转储是java_pidpid.hprof在 JVM 工作目录中调用的文件中创建的。您可以使用该-XX:HeapDumpPath选项指定替代文件名或目录。您还可以使用JConsole观察峰值堆内存使用情况。另请参阅如何监视堆/永久生成和 gc 活动的 Java 内存使用情况

分析堆转储

堆转储可以显示大对象是否被保留了很长时间。有时,要找到保留的原因,您需要了解导致内存不足错误的应用程序代码和情况。该问题可能是由应用程序代码中的内存泄漏引起的;否则,问题可能是为应用程序的峰值负载分配的内存不足。

保留大对象的原因包括:

  • 吸收分配给 JVM 的所有堆内存的单个对象。
  • 许多小对象保留内存。
  • 单个线程(可能是与OutOfMemoryError.

确定大量保留的来源后,查看垃圾收集根的路径以了解是什么使对象保持活动状态。垃圾收集根是堆外的对象,因此永远不会被收集。垃圾收集根的路径显示了阻止堆上的对象被垃圾收集的引用链。这篇文章的10个技巧使用Eclipse内存分析器,用于分析堆转储和内存泄漏在提供建议Eclipse的内存分析器

此外,有关分析堆转储的更多详细信息,请参阅如何分析 Java堆转储。如果应用程序代码没有问题,请增加 Java 堆的大小以满足负载要求。

影响内存使用的 JVM 选项

影响 JVM 可用内存的参数包括:

  • -Xms:设置堆的最小和初始大小。
  • -Xmx: 设置堆的最大大小。
  • -XX:PermSize:设置永久代(perm)内存区域的初始大小。此选项在 JDK 8 之前可用,但不再受支持。
  • -XX:MaxPermSize: 设置永久内存区域的最大大小。此选项在 JDK 8 之前可用,但不再受支持。
  • -XX:MetaspaceSize: 设置元空间的初始大小。此选项从 JDK 8 开始可用。
  • -XX:MaxMetaspaceSize: 设置元空间的最大大小。此选项从 JDK 8 开始可用。

生产环境通常将-Xms-Xmx选项设置为相同的值,以便堆大小固定并预先分配给 JVM。

有关不同 JVM 选项及其使用的更多信息,请参阅 Oracle 的java 命令选项列表。

计算JVM内存消耗

许多程序员正确地计算出应用程序 JVM 的最大堆值,但发现 JVM 使用了更多内存。该-Xmx参数的值指定了 Java 堆的最大大小,但这并不是 JVM 消耗的唯一内存。永久代(JDK 8 之前的名称)或元空间(JDK 8 以后的名称)、CodeCache、其他 JVM 内部使用的本机 C++ 堆、线程堆栈空间、直接字节缓冲区、垃圾收集开销等东西被算作JVM内存消耗的一部分。

您可以按如下方式计算 JVM 进程使用的内存:

JVM memory = Heap memory+ Metaspace + CodeCache + (ThreadStackSize * Number of Threads) + DirectByteBuffers + Jvm-native
片段

因此,JVM 内存使用量可能会超过-Xmx峰值业务负载下的值。

JVM 内存消耗的组成部分

以下列表描述了 JVM 内存的三个重要组件:

  • 元空间:存储有关应用程序中使用的类和方法的信息。这个存储区域在JDK 8 之前的HotSpot JVM 中被称为 Permanent Generation 或perm,并且该区域与 Java 堆是连续的。从 JDK 8 开始,永久代已被元空间取代,元空间与 Java 堆不连续。元空间在本机内存中分配。这MaxMetaspaceSize参数限制了 JVM 对 Metaspace 的使用。默认情况下,Metaspace 没有限制,它从一个很小的默认大小开始,并根据需要逐渐增长。元空间只包含类元数据;所有实时 Java 对象都移动到堆内存中。所以元空间的大小比永久代要小得多。通常,除非您面临较大的 Metaspace 泄漏,否则无需指定最大 Metaspace 大小。
  • CodeCache:包含由 JVM 生成的本机代码。JVM 生成本机代码的原因有很多,包括动态生成的解释器循环、Java 本机接口 (JNI) 存根以及由即时 (JIT) 编译器编译为本机代码的 Java 方法。JIT 编译器是 CodeCache 区域的主要贡献者。
  • ThreadStackSize:使用该-XX:ThreadStackSize=<size>选项以字节为单位设置线程堆栈大小,也可以指定为-Xss=<size>. 附加字母kK以指示千字节;mM表示兆字节;或gG表示千兆字节。的默认值-XX:ThreadStackSize取决于底层操作系统和体系结构。

如何检查线程堆栈大小

您可以使用以下命令检查当前线程堆栈大小:

$ jinfo -flag ThreadStackSize JAVA_PID

使用以下命令检查默认线程堆栈大小:

$ java -XX:+PrintFlagsFinal -version |grep ThreadStackSize

图 1 显示了上一个命令显示的线程堆栈大小。

您可以检查程序使用的线程堆栈的大小。
图 1:检查程序使用的线程堆栈的大小。

结论

根据应用程序要求设置堆大小是配置堆设置的第一步。该选项限制堆大小。但是,JVM 需要更多超出堆大小的内存,包括 Metaspace、CodeCache、线程堆栈大小和本机内存。因此,当您考虑 JVM 的内存使用情况时,一定要包括这些其他部分的内存消耗。

本系列的下一篇文章将介绍不同的垃圾收集器以及为您的应用程序选择最佳垃圾收集器的指南。

来自:https://developers.redhat.com/articles/2021/09/09/how-jvm-uses-and-allocates-memory