第6章:配置和监控JVM的内存管理

使用VisualVM监控Java进程

从应用程序标签页中,我们可以看到正在运行的Java进程列表,如图6.3所示。在这里,我们可以简单地选择需要检查的进程。让我们启动示例Java应用程序,该程序将创建一个巨大的字符串列表。

Figure

图6.3 – Java进程概览

现在我们可以选择要分析的进程。在本例中,我们想要分析PID为6450的进程。点击该进程后,我们会看到进程的概览信息,如图6.4所示。

Figure

图6.4 – Java进程概览

在图6.4所示的概览中,我们可以看到数据的摘要。我们能看到正在分析的进程、运行的JVM和Java版本,以及启动应用程序时使用的JVM参数。VisualVM还能提供更多详细数据。顶部有多个标签页:概览(Overview)监视(Monitor)线程(Threads)采样器(Sampler)分析器(Profiler)。我们已经看过概览标签页;接下来看图6.5中监视标签页下的数据。

Figure

图6.5 – 使用VisualVM监控Java进程

这里我们会看到应用程序内部运行状况的详细细节。有四个图表。左上角的图表显示CPU使用率,可以看到这个程序占用了相当多的CPU。该图表还显示了垃圾收集(GC)活动,整体来看非常低。这是合理的,因为本来就没有多少需要回收的对象。垃圾收集活动结合右上角的内存图,能够很好地反映应用程序在内存方面的健康状况。如果垃圾收集器工作非常频繁(如左上角图表中显示的GC活动很多),而且内存持续增长(右上角图表中下方的线代表已用堆),则意味着我们可能遇到了内存泄漏问题。基本上,如果GC周期过于频繁,就说明需要深入检查GC和内存是否存在问题。如果检查后GC周期仍然过于频繁,且内存也没有下降,那么这就是一个红色警报,必须立即调查。实际上,当JVM花费超过98%的时间进行GC,且回收的堆内存不足2%时,就会抛出 OutOfMemoryError: GC Overhead limit exceeded

底部的两个图表显示已加载的Java类(左侧)和应用程序中的线程(右侧)。我们可以通过切换到 线程(Threads) 标签页来获取更多关于线程的详细信息。在图6.6中,我们看到了应用程序中线程的概览。

Figure

图6.6 – 应用程序中的线程

在最左侧我们可以看到线程的名称。条状图表示线程随时间变化的状态——例如,运行中或等待中。然后我们可以看到它们运行的时间。

采样器(Sampler) 标签页中,如图6.7所示,我们可以看到CPU或内存的状况。

Figure

图6.7 – VisualVM中的采样器标签页

我们现在查看的是内存采样,它显示了有多少存活对象以及某个类占用了多少空间。这里,字节数组占用的空间最大。这很合理,因为字符串的值存储在字节数组中。您还可以按线程过滤此概览,或者查看CPU的性能。

在最后一个标签页中,我们可以看到 分析(Profiling)。分析(Profiling)和采样(Sampling)用于类似的目的,但过程不同。采样是通过生成线程转储并分析这些线程转储来完成的。分析则需要在应用程序中添加一些逻辑,以便在事件发生时发出信号。这会相当程度地影响应用程序的性能。因此,不建议在生产环境中运行的应用程序上执行分析操作。不过,分析可以提供很多洞察。

图6.8显示了分析所有类的结果。这里您可以看到与采样类似的结果(尽管当时分配的对象较少)。在这种情况下,采样同样有效。

Figure

图6.8 – 分析所有类

VisualVM非常适合快速、直观地了解应用程序内存的情况。这在调整JVM和检查结果时尤其有用。在下一节中,我们将进行这项操作——学习如何调整JVM的配置并观察这些调整的影响。


调整JVM的配置

JVM的设置是可以调整的。调整JVM设置的过程称为调优(tuning)。这些调整的目的是提升JVM的性能。再次强调,调优不应该是提升性能的第一步。良好的代码始终应该放在首位。

我们将研究与内存管理相关的设置:堆大小、元空间(Metaspace)以及垃圾收集器。

调整堆大小和线程栈大小

堆大小是可以更改的。通常最佳实践是将堆大小设置为不超过服务器可用内存的一半。否则可能导致性能问题,因为服务器上还有其他进程在运行。

默认大小取决于系统。以下命令可显示Windows系统上的默认值:

java -XX:+PrintFlagsFinal -version | findstr HeapSize

以下命令显示macOS系统上的默认输出:

java -XX:+PrintFlagsFinal -version | grep HeapSize

输出以字节为单位。您可以在图6.9中看到我电脑上的输出。

Figure

图6.9 – macOS系统上的输出

堆的大小会影响垃圾收集。起初这似乎有些反直觉,但让我们做一个简单的思想实验。如果我们拥有无限的堆内存,我们还需要垃圾收集吗?不需要,对吧?既然不需要释放内存,为什么还要运行如此昂贵的进程呢?

堆越小,垃圾收集器就需要越频繁地工作,因为内存更容易被填满,它需要更努力地腾出空间。然而,堆越大,一次完整的垃圾收集周期所需的时间就越长。毕竟要扫描的对象更多。一个经验法则是,希望垃圾收集花费的时间不超过应用程序执行时间的5%。

实际的调优方法因服务器而异。这里我们将演示如何在启动应用程序时通过命令行进行调优。请注意,我们设置的选项名称在不同服务器上是相同的,但设置方法和位置可能不同。

启动Java应用程序时,我们可以使用不同的内存选项。我们可以指定内存池的初始大小、最大内存池,以及线程栈大小。以下是将所有大小设置为1024 MB的方法:

java -Xms1024m -Xmx1024m -Xss1024m ExampleAnalysis

如果要设置为不同的大小,只需相应地调整选项值即可。可以使用以下命令启动一个具有调整后内存设置的Java应用程序(在64位系统上):

java -Xms4g -Xmx6g ExampleAnalysis

这将启动我们的示例Java应用程序,初始堆大小为4 GB,最大堆大小为6 GB。

  • -Xms1024m(初始堆大小)
  • -Xmx1024m(最大堆大小)
  • -Xss1024m(线程栈大小)

类似于通过 -Xmx-Xms 绑定总堆大小,我们可以通过以下方式绑定年轻代(young generation)的大小:

-XX:MaxNewSize=1024m   (最大新生代大小)
-XX:NewSize=1024m      (最小新生代大小)

这里我们将最小和最大大小都设置为1024 MB。我们可能会耗尽内存,这将导致 OutOfMemoryError。接下来我们将看到如何在这种情况下获取堆转储(heap dump),以便检查问题所在。

记录低内存(堆转储)

当应用程序因内存不足错误而结束时,获取堆转储非常有帮助。堆转储是应用程序内存中对象的快照。在这种情况下,我们可以检查在内存耗尽时应用程序中存在的对象。这样,我们就可以查看哪个对象可能导致内存溢出。

如果希望在发生 OutOfMemoryError 异常时让JVM创建堆转储,可以在启动JVM时使用以下JVM参数:

java -XX:+HeapDumpOnOutOfMemoryError ExampleAnalysis

我们还可以指定路径:

java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/some/path/to/dumps ExampleAnalysis

这样,堆转储将存储在指定的路径中。创建堆转储的方法有很多种——例如,如果应用程序没有因为 OutOfMemoryError 崩溃,也可以使用 jmap 创建应用程序的堆转储。

调整元空间(Metaspace)

元空间的默认行为相当特殊,因为它似乎有一个限制。这很容易被误解,因为这个限制并不是真正的限制。当达到这个限制时,它会检查是否可以进行垃圾收集,然后进行扩展。因此,仔细设置以下变量非常重要:

  • 最大大小,使用 -XX:MaxMetaspaceSize=2048m
  • 垃圾收集的阈值,使用 -XX:MetaspaceSize=1024m
  • 最小和最大空闲比率(free ratio),使用:
    -XX:MinMetaspaceFreeRatio=50
    -XX:MaxMetaspaceFreeRatio=50

最小和最大空闲比率对于计划动态加载大量类非常有用。通过确保有足够的内存可用,可以提高动态加载类的速度。这是因为为需要加载的类释放内存需要占用CPU时间。通过选择足够大的空闲比率并确保内存可用,我们可以跳过需要分配额外内存的步骤。在前面的示例中,它们都被设置为50%。

垃圾收集调优

您可能已经意识到,垃圾收集是一个昂贵的进程。优化它可以真正帮助提升应用程序的性能。您自己不能触发垃圾收集;这是由JVM决定的。您可能听说过以下向JVM建议垃圾收集的方法:

System.gc();

这并不能保证垃圾收集一定会发生。所以,您不能触发垃圾收集,但可以影响JVM处理垃圾收集的方式。

然而,在调整任何与垃圾收集相关的设置之前,务必确保您确切了解自己在做什么。为此,您需要具备扎实的垃圾收集器知识。

另外,在调整任何设置之前,必须先查看内存使用情况。确保了解哪些空间被填满以及何时发生。一个健康的堆在VisualVM中看起来有点像锯齿。内存使用量上下波动,形成尖峰,类似于锯齿。它有一定的已用内存量,然后垃圾收集到来,将已用内存降低到某个基准水平。之后内存再次增长,然后在大约相同的使用水平上,垃圾收集再次到来,将其降低到基准水平,如此往复。

如果您看到内存随时间增长,且每次垃圾收集结束时基准水平都略高,那么很可能存在需要处理的内存泄漏。正如我们在第4章中看到的,有几种不同的垃圾收集器实现可供选择。启动JVM时,我们也可以选择要使用的垃圾收集器:

  • -XX:+UseSerialGC
  • -XX:-UseParallelGC
  • -XX:+UseConcMarkSweepGC
  • -XX:+G1GC
  • -XX:+UseZGC

并非所有系统上都支持所有这些选项,而且每种垃圾收集器都有自己的额外选项。例如,我们可以选择并行垃圾收集器并指定垃圾收集器的线程数:

java -XX:+UseParallelGC -XX:ParallelGCThreads=4 ExampleAnalysis

这就是如何使用并行垃圾收集器启动应用程序,并为其分配4个线程。所有垃圾收集器的选项过于详细,无法在此一一讨论。详细内容可以在您所使用的Java实现的官方文档中找到。以下是Oracle实现的链接(但请注意,当您阅读本书时,可能已经发布了更新版本):

Info

Warning

重要提示:垃圾收集调优应基于对应用程序内存行为的深入理解。不要随意更改设置,否则可能导致性能下降或不可预知的行为。始终先通过监控工具(如VisualVM)了解当前内存状况,再进行针对性调整。

总结

在本章中,我们探讨了调优JVM时需要注意的事项。我们需要关注内存运行、延迟和吞吐量。

为了监控应用程序的表现,我们可以使用分析器。我们了解了如何使用JDK默认自带的jstat命令行工具。之后,我们还学习了如何使用VisualVM以更直观的图形化方式了解运行状况。

接着,我们讨论了如何调整应用程序的堆、元空间(Metaspace)以及垃圾回收器。我们还通过简单的示例程序观察了这些调整所带来的效果。

再次强调

请牢记,调整JVM以提升性能应始终作为最后一步;更显而易见的优化手段,例如改进代码,应始终优先执行。

掌握这些知识后,你已经准备好进入下一章,学习如何避免内存泄漏。


参考链接:Oracle官方垃圾回收调优指南
https://docs.oracle.com/javase/9/gctuning/introduction-garbage-collection-tuning.htm

6.0 第6章:配置和监控JVM的内存管理

图片引用

以下为原始文本中的图片占位符,保留以保持完整性:

  • [Image 826 on Page 129]
  • [Image 830 on Page 131]
  • [Image 832 on Page 132]
  • [Image 834 on Page 133]
  • [Image 836 on Page 134]
  • [Image 839 on Page 136]
  • [Image 841 on Page 137]
  • [Image 844 on Page 139]
  • [Image 846 on Page 140]

6.0 第6章:配置和监控JVM的内存管理


图片内容分析

第129页图片

图片无法正常显示,内容为未知。根据上下文,该图片可能涉及JVM内存管理的配置方式或监控示意图。

第131页图片

无法获取图片内容,请提供图片。

第132页图片

图片无法显示,无法分析其具体内容。

第133页图片

图片不可见,无法分析其具体内容。

第134页图片

图片无法显示,根据上下文推测内容可能与JVM内存管理的配置和监控相关。

第136页图片

由于图片格式不支持,无法分析图片内容。

第137页图片

根据上下文,图片可能展示了JVM内存管理的配置选项或性能监控界面截图。文档讨论了不同内存区域及如何配置JVM的内存管理方式,但没有具体图表或表格信息。由于图片无法显示,无法准确重建内容。建议提供清晰的图片以便分析。

第139页图片

图片无法加载,无法分析具体内容。根据上下文,可能是一张关于JVM内存管理配置或监控的示意图或流程图,但无法确认。请重新提供可识别的图片。

第140页图片

图片无法显示,无法分析其具体内容。