垃圾收集日志是一个重要的信息来源,对于与性能相关的一些悬而未决的案例分析特别有用,例如它提供了一些关于崩溃发生原因的见解。它使得分析人员甚至可以在没有活跃的应用程序进程可供诊断的情况下开始工作。
每一个严肃的应用程序都应该始终做到:
• 生成垃圾收集日志;
• 将其保存在一个与应用程序的输出独立的文件中。
对于生产型应用程序来说尤其如此。正如我们将看到的,垃圾收集日志并没有明显可观测到的开销,所以对于任何重要的 JVM进程来说,它应该始终开启。
首先要做的是在应用程序启动时添加一些开关。最好把这些开关看作必须打开的垃圾收集日志标志,任何 Java/JVM 应用程序(也许桌面应用程序可以除外)都应该启用。这些标志是:
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintTenuringDistribution
-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
接下来详细了解一下这些标志,其用法如下表所示。
性能工程师应该注意到关于这些标志的以下一些细节。
• PrintGCDetails 标志取代了比较旧的 verbose:gc 标志。应用程序应该删除原来的标志。
• PrintTenuringDistribution 标志与其他标志不同,因为它提供的信息人很难直接处理,需要使用工具。该标志提供了计算关键的内存压力影响和事件(如过早晋升)所需的原始数据。
• PrintGCDateStamps 和 PrintGCTimeStamps 都是必需的,因为前者用于将垃圾收集事件与应用程序事件(在应用程序日志文件中)相关联,后者用于将垃圾收集和其他内部JVM 事件相关联。
这种细节程度的日志记录不会对 JVM 的性能产生可测量的影响。当然,产生日志的量取决于许多因素,包括分配率、使用的收集器和堆大小(堆越小,则需要更频繁地进行垃圾收集,因此生成日志的速度也越快)。
除了必须使用的这些标志之外,还有一些控制垃圾收集的日志滚动的标志,许多应用程序支持团队会发现它们在生产环境中很有用。
要设置合理的日志滚动策略,应该与运维人员(包括 DevOps 人员)一起讨论决定。这种策略的选择以及对恰当的日志和工具的讨论不在本书范围之内。
在之前的文章中曾介绍过 VisualGC 工具,它能够实时显示 JVM 的堆状态。该工具实际上依靠JMX(Java Management eXtension)接口来收集 JVM 的数据。关于 JMX 的完整讨论就不扩展了,有兴趣的朋友可以去官网查看技术文档,但就 JMX 对垃圾收集的影响而言,性能工程师应该注意以下几点。
• 垃圾收集日志数据是由实际的垃圾收集事件驱动的,而 JMX 的来源数据是通过采样获得的。
• 垃圾收集日志数据获取的成本极低,而 JMX 存在隐含的代理和远程方法调用(remotemethod invocation, RMI)成本。
• 垃圾收集日志数据包含了与 Java 内存管理相关的性能数据的 50 多个方面,而 JMX 只有不到 10 个。
传统上,作为性能数据的来源, JMX 优于日志的一个方面是它可以提供开箱即用的流式数据。
通过 JMX 获得的 Bean 是标准化的,而且很容易访问。 VisualVM 工具提供了一种将该数据可视化显示的方式,市场上也有很多其他工具可用。
使用 JMX 监控应用程序的客户端,通常要依赖对运行时进行采样以获得当前状态的更新。为了连续获得数据,客户端需要轮询运行时中的 JMX Bean。
在存在垃圾收集的情况下,这会导致一个问题:客户端无法知道收集器何时运行,进而也意味着每个收集周期前后的内存状态都是未知的。因此,我们无法对垃圾收集数据执行一系列更深入和更精确的分析技术。
即便如此,基于 JMX 数据的分析仍然有用,但仅限于确定长期趋势。然而,如果想准确地调优某个垃圾收集器,我们需要做得更好。特别是能够了解每次收集前后堆的状态是非常有用的。
此外,还有一组围绕内存压力的极其重要的分析(即分配率)也因为从 JMX 收集数据的这种方式而不再能够执行。不仅如此, JMXConnector 规范目前的实现依赖于 RMI。因此,使用 RMI 通信通道会遇到的任何问题,使用 JMX 也同样会遇到。具体包括:
• 打开防火墙中的端口,以便可以建立后续的套接字连接;
• 使用代理对象以方便调用 remove() 方法;
• 依赖于 Java 终结化(finalization)。
对于少数 RMI 连接来说,关闭连接所需的工作量微乎其微。然而, 清理工作要依赖于终结化。这意味着必须运行垃圾收集器以回收该对象。
JMX 连接的生命周期这一性质在大多数情况下会导致 RMI 对象直到一次 Full GC 时才会被收集。
默认情况下,任何使用 RMI 的应用程序每小时都会触发 Full GC。对于已经使用 RMI 的应用程序,使用 JMX 不会增加成本。但是,对于还没有使用 RMI 的应用程序,如果决定使用 JMX,必然存在额外的影响。
现代的垃圾收集器包含了很多不同的活动部件,将其组合在一起得到的是一个非常复杂的实现。实现如此复杂,以致某个收集器的性能即使不是不可能的,也是很难预测的。这些类型的软件系统就是所谓的浮现式的(emergent),因为它们的最终行为和性能是所有组件如何共同工作和执行的结果。不同的压力会以不同方式影响不同的组件,从而导致成本模型也会动态发生变化。
最初, Java 垃圾收集的开发人员添加了垃圾收集日志来帮助调试其实现,因此, 60 个左右的垃圾收集相关标志所产生的数据中,有很大一部分是出于性能调试的目的。随着时间的推移,负责对应用程序中的垃圾收集过程进行调优的人开始意识到,鉴于垃圾收集调优的复杂性,对运行时中正在发生的事情有个精确的了解也会让他们受益匪浅。因此,不管采用何种调优方式,能够收集和阅读垃圾收集日志都是很有帮助的。
垃圾收集日志是在 HotSpot JVM 内部使用非阻塞写入机制来完成的。它对应用程序的性能没有影响,所有生产环境中的应用程序都应该开启垃圾收集日志。
不同于语言规范和虚拟机规范,垃圾收集日志消息并没有一个标准的格式。 HotSpot 垃圾收集开发团队可以自行决定任何一条消息的内容。不同的小版本之间,格式都有可能发生改变,而且确实存在改变的情况。
虽然最简单的日志格式很容易解析,但随着垃圾收集日志标志的添加,所产生的日志输出就变得更为复杂了,因而情况也就进一步复杂化。并发收集器生成的日志更是如此。经常有这样的情况发生,即系统采用了手写的垃圾收集日志解析器,而有人修改了垃圾收集配置,这使得日志输出格式发生了变化,从而导致在此之后的某个时间点,解析器不能继续工作。调查其原因,矛头指向了垃圾收集日志,团队发现自己开发的解析器无法处理改变后的日志格式——解析器在日志信息最有价值的那个点停止了工作。
不建议开发人员自己解析垃圾收集日志。相反,应该使用某个工具。
GCViewer 是一个桌面工具,提供了一些基本的垃圾收集日志解析和图形化显示功能,其最大的优点在于它是开源软件,可以免费使用。然而,它提供的功能没有商业工具那么多。要使用 GCViewer,需要下载源代码。在编译和构建后,可以将其打包成一个可执行的 JAR文件。
然后可以在 GCViewer 主界面中打开垃圾收集日志文件,示例如下图所示。
GCViewer 缺乏分析能力,对于 HotSpot 可能生成的各种垃圾收集日志格式,它只能解析一部分。
可以将 GCViewer 用作解析库,将数据点导出到一个可以可视化显示的工具中,但这需要在现有开源代码的基础上进行额外的开发。
本章重点介绍了 垃圾收集日志相关知识和工具的使用。