第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页图片
图片无法显示,无法分析其具体内容。