3.4 Java任务控制

商业版 Java 7(从 7u40 开始)和 Java 8 包含了称为 Java Mission Control(以下称 JMC)的监控新特性。JDK 6 JRockit JVM(JMC 的技术源自于此)的用户对这个特性应该很熟悉,Oracle 将它作为 Java 7 的一部分合并了进来。JMC 不是开源版 Java 的一部分,并且只有通过商业许可才可用(也就是说,和其他公司的有竞争力的监控工具的许可程序相类似)。

JMC 的程序(jmc)开启一个窗口以显示当前机器上的 JVM 进程,你可以选择一个或多个进行监控。图 3-8 是 JMC 监控 GlassFish 应用服务器的一个实例。

{%}

图 3-8:Java 任务控制器监控

图中显示了 JMC 正在监控的基本信息:CPU 使用率和堆使用量。但请注意,CPU 图包括了当前机器上所有的 CPU(基本上是 100%,即使这里被监控的应用只用了 70%)。这是监控的关键特性:JMC 可以监控整个系统,而不仅是所选择的 JVM。顶部的“面板”可以配置,以显示 JVM 的信息(所有的统计信息,包括 GC、类加载、线程使用情况、堆使用量等)以及操作系统相关的信息(机器总的 CPU 和内存使用量、页面交换、平均负载等)。

像其他监控工具一样,JMC 可以向任何被监控应用的 MBean 发起 Java Management Extensions(JMX)调用。

3.4.1 Java飞行记录器

JMC 的关键特性是 Java 飞行记录器(Java Flight Recorder,JFR)。正像它名字所暗示的,JFR 数据是 JVM 的历史事件,这些可以用来诊断 JVM 的历史性能和操作。

JFR 的基本操作是开启一组事件(例如,线程等待某个锁而被阻塞的事件)。每当选择的事件发生时,就会保存相应的数据(保存在内存或文件中)。数据流保存在循环缓冲中,所以只有最近的事件。JMC 可以显示这些事件——实时从 JVM 获取或者从文件读取——你可以对这些事件进行分析,诊断性能问题。

所有这些——事件的类别、循环缓冲的大小、数据保存在哪里等——都可以通过 JVM 的不同参数、JMC 的 GUI 以及程序运行时的 jcmd 命令来控制。JFR 的默认设置只有很低的开销:程序性能的 1% 以下。如果开启的事件变多,或者事件触发的阈值更改等,开销也会随之变化。本节的后面将详细讨论这些配置,我们先了解一下这些事件显示出来是什么样的,从而更容易理解 JFR 是如何工作的。

1. JFR概览

示例中的 JFR 记录了 GlassFish 应用服务器 6 分钟的数据。服务器运行的是第 2 章所讨论的股票 servlet。JMC 加载这些记录后,首先看到的是基本的监控概要(图 3-9)。

{%}

图 3-9:JFR 常规信息

上图所展示的与 JMC 基本监控时的显示非常类似。仪表盘显示的是 CPU 使用率和堆使用量,仪表盘上方是事件的时间线(用一系列的垂直条表示)。时间线可以放大以显示重点区域。本示例记录的数据区间为 6 分钟,放大之后,显示的是到记录结束之前 1:06 分钟内的数据。

图中的 CPU 占用率 2 大体可以看出正在做什么。GlashFish 的 JVM 在图的底部(平均大约 70% 的占用率),而机器的 CPU 为 100%。面板底部还有一些选项卡可以一探究竟:系统属性,JFR 记录的实际数据。窗口左边面板上的图标更为重要:通过这些图标对应的视图可以了解应用内部的行为。

2中文 JMC 中的显示。——译者注

2. JFR内存视图

这里汇集的信息范围很广。图 3-10 只显示了内存视图中的一个面板。

{%}

图 3-10:JFR 内存面板

图中显示,内存的使用量随着新生代的回收而发生很有规律的波动(而更重要的是,该应用的内存堆整体没有什么增长:没有对象被提升到老年代)。左下角面板显示 JFR 记录期间发生的所有垃圾收集(每行表示一次收集),包括持续的时间和收集的类型(这个例子始终是 ParallelScavenge)。当选中某一行事件时,右下角面板会进一步分拆显示事件的详细信息,包括这次垃圾收集的所有特定阶段和花了多长时间。

正如大家从这页的不同选项卡上所看到的,还有许多其他有价值的信息:有多少引用对象和多长时间被清理,并发收集过程中是否有对象提升或疏散失败(evacuation failure),GC 算法的配置参数(包括代的大小和 Survivor 空间的配置),甚至已分配的特定种类对象的信息。当你读到第 5 章和第 6 章时,希望你还能牢记这里所讨论的这个工具所能诊断的问题。如果你需要弄明白为何 CMS 出现并发失败而执行全部 GC(因为晋升失败?),或者 JVM 如何调整晋升阈值(tenuring threshold),或者关于 GC 及其行为的几乎所有数据,JFR 都将回答。

3. JFR代码视图

JFR3 的“代码”视图显示所记录的基本性能分析信息(图 3-11)。

3原文是“Java Mission Control”,说 JFR 更为准确。——译者注

{%}

图 3-11:JFR 代码面板

这个视图的第一个选项卡显示了一组包名,这是一个许多分析器中都没有的重要特性。不出所料,股票应用的 41% 的时间用在 java.math 包内的计算上。视图底部还有其他传统分析视图的选项卡:最热的方法和被分析代码的调用树。

与其他分析器不同的是,JFR 提供了查看代码内部的其他模式。“异常错误”(Throwable)选项卡提供了应用内部异常处理的视图(第 12 章将讨论为什么异常处理过多会导致糟糕的性能)。还有些选项卡告诉我们编译器正在做什么,包括代码缓存的内部视图(参见第 4 章)。

还有其他显示——如线程、I/O 和系统事件——但这其中的大部分只是为 JFR 记录的真实事件提供了更好的视图。

4. JFR事件概览

JFR 生成的事件流会作为记录保存下来。上述视图可以展示这些事件,但查看事件最重要的方法是“事件”面板自身,参见图 3-12。

{%}

图 3-12 :JFR 事件面板

窗口左边的面板可以过滤需要显示的事件。这里只选择应用级别的事件。请注意,在开始记录时,只包括特定种类的事件:在这个时候,我们进行事后的过滤(下节演示如何在记录时过滤所包括的事件)。

本示例的 66 秒间隔中,应用的 JVM 产生了 10 612 个事件,JDK 库产生了 1536 个事件,靠近窗口底部的是在这个期间生成的 6 种事件类型。我已经讨论过这个示例的线程停止(thread-park)和管程等待(monitor-wait)事件为什么这么高。可以忽略这些事件(事实上,通常它们是很好的过滤条件)。那其他事件呢?

过了 66 秒后,应用的多个线程写 socket 花费了 40 秒。对于在 4 个 CPU 上运行的应用服务器来说,这并不是一个不合理的数字(也就是说,264 秒),但意味着可以通过向客户端写入较少的数据来改善性能(使用第 10 章列出的技术)。

与此类似,多个线程读 socket 用了 143 秒。这个数字听起来比较糟糕,值得进一步追踪这些事件,不过最终证实有些线程使用了阻塞 I/O 读取预计周期性到达的管理请求。这些请求之间——很长一段时间——线程被 read() 方法阻塞。所以这里的读取时间变得可以接受:如果使用性能分析器,你应该判断大量的线程被 I/O 阻塞是预料之中的,还是说明存在性能问题。

还有监视器阻塞事件。正如第 9 章所讨论的,锁竞争分为两种级别:首先是线程自旋等待锁,然后线程使用(进程中称为锁膨胀)一些 CPU 或 OS 特定的代码等待锁。标准的分析器可以提示处于那种情形,因为自旋时间包括在该方法的 CPU 时间内。本地分析器可以给出锁膨胀的信息,但这是不确定的(例如,Oracle Studio 分析器在 Solaris 上运行的很好,但 Linux 上缺少必要的能提供与 Solaris 相同信息的操作系统钩子)。但 JVM 可以直接向 JFR 提供所有的这些数据。

使用锁可见性的例子请参见第 9 章。不过通常来说,理所当然的那些 JFR 事件是直接来自于 JVM 的事件,它们给应用增加了一个可见性级别,这是其他工具不能提供的。在 Java 7u40 中,JFR 可以监控 77 种事件类型。根据发布的版本,事件种类的精确数字会略微有所不同,这里是一些最有用的。

以下每种事件类型的说明都包括两点。事件可以像其他工具如 jconsolejcmd 那样收集基本信息。这类信息在第一点中描述。第二点则描述的是事件提供者,这在 JFR 之外很难获得。

Classloading

  • 装载或卸载的类的数量

  • 哪个类装载器装载的类,装载单个类需要的时间

Thread statistics

  • 创建和销毁的线程数,线程转储

  • 被锁阻塞的线程(以及阻塞住它们的特定锁)

Throwables

  • 应用所用的异常错误类

  • 有多少异常和错误被抛出了,以及它们生成时的栈追踪信息

TLAB allocation

  • 堆分配的数量和本地线程分配缓冲的大小

  • 堆中分配的对象和分配它们的栈追踪信息

File and socket I/O

  • 执行 I/O 所用的时间

  • 每次读 / 写调用所用的时间,读或写很长时间的特定文件或 socket Monitor blocked

  • 等待管程的线程

  • 在特定管程上阻塞的线程以及它们被阻塞的时间

Code cache

  • 代码缓存的大小以及包含多少量

  • 从代码缓存中移走的 Java 方法,代码缓存配置

Code compilation

  • 哪个方法已被编译,OSR 编译和编译的时长

  • 除了 JFR 其他工具也包括的信息,但这是从多个源头获取的统一信息

Garbage collection

  • GC 的时间,包括单个阶段,代的大小

  • 除了 JFR 其他工具也包括的信息,但这是从多个工具获取的统一信息

Profiling

  • 探查和采样分析器

  • 不像真的性能分析器那样有很多信息,但 JFR 分析器提供了很好的更高层次的概要信息

3.4.2 开启JFR

在 Oracle JVM 的商业版本中,JFR 初始时为关闭。为了开启它,可以在应用的启动命令行上添加标志 -XX:+UnlockCommercialFeatures -XX:+flightRecorder。这会开启 JFR 特性,但直到记录过程自身开始时才会记录信息。记录可以通过 GUI 或者命令行产生。

1. 通过JMC开启JFR

开启记录的最简单方法是通过 JMC GUI(jmc)。当 jmc 启动时,它会列出运行在当前系统中的所有 JVM 进程。JVM 进程显示在树状的节点配置中。双击 4“飞行记录器”(Flight Recorder)标签下的节点会弹出像图 3-13 这样的飞行记录窗口。

4原文为“expand”,参考实际操作应为双击。——译者注

{%}

图 3-13:JFR 启动飞行记录窗口

飞行记录器可以设为两种模式之一:固定持续时间(这个例子是 1 分钟),或者持续进行。对于持续记录,会有一个循环缓冲,缓冲包含最近的事件,并且这些事件在设置的持续时间和大小内。

为了进行主动分析——意味着你将开启记录,然后还会产生一些工作量,或者在 JVM 热身之后——应该用固定持续时间的方式记录,在负载测试试验中开启记录。这些记录将对测试期间 JVM 如何响应给出很好的指示。

持续记录最适合于响应性分析(reactive analysis)。JVM 会保留最近的事件,然后转储为响应某些事件的记录。例如,WebLogic 应用服务器可以在遇到应用服务器异常事件时(例如处理超过 5 分钟的请求),触发转储记录。你可以针对任何事件,设立自己的监控工具转储这些记录。

2. 通过命令行开启JFR

开启 JFR 之后(选项 -XX:+flightRecorder),有两种方法控制记录应该在何时以及如何发生。JVM 用 -XX:+flightRecorderOptions=string 参数方式启动时,可以控制这些记录参数。这对响应性记录来说最为有用。参数中的 string 是一列逗号分隔的名字 - 值对,可以用以下选项:

name=name

  用以标识记录的名字。

defaultrecording=

  表示初始时是否开启记录。默认为 false。对于响应性分析,应该设为 true

settings=path

  JFR 设置文件的文件名(参见下节)。

delay=time

  记录开始前延迟的时间量(例如,30 秒,1 小时)。

duration=time

  记录持续的时间。

filename=path

  记录文件名。

compress=

  记录是否开启压缩(gzip)。默认为 false

maxage=time

  循环缓冲中保留记录数据的最长时间。

maxsize=size

  记录循环缓冲的最大尺寸(例如,1023 K,1 M)。

在某些情况下,像上述那样设置默认记录很有用,但为了更大的灵活性,所有选项可在程序运行时(假设 -XX:+flightRecorder 已先指定),用 jcmd 来控制。

要开启飞行记录:

  1. % jcmd process_id JFR.start [options_list]

options_list 是一组用逗号分隔的名字 - 值对,控制记录如何进行。可能的选项与命令行使用 -XX:+flightRecorderOptions=string 时的标志完全一样。

如果开启持续记录,可以在任何时间,通过以下命令将当前循环缓冲里的数据转储到文件中:

  1. % jcmd process_id JFR.dump [options_list]

选项列表包括:

name=name

  在这个名字下的记录已经开始。

recording=n

  JFR 记录的编号。

filename=path

  转储文件的位置。

对于给定的进程,可能开启了多个 JFR 记录。以下命令可查看开启的记录:

  1. % jcmd process_id JFR.check [verbose]

这个例子中,用记录启动时的名字来标识它们,也可以任意指定记录的编号(可以在其他 JFR 命令中使用)。 最后是进程放弃记录的命令:

  1. % jcmd process_id JFR.stop [options_list]

这个命令接受以下选项:

name=name

  停止记录的名字。

recording=n

  停止记录的编号(可由 JFR.check 获得)。

discard=boolean

  如果为 true,则丢弃数据而不是写到前面所提供的文件中(如果有的话)。

filename=path

  数据写到给定的路径上。

回归检查运行系统的时候,在自动化性能测试系统中运行这些命令并生成记录,会很有用。

3.4.3 选择JFR事件

JFR(当前的)支持大约 77 种事件。大多数是周期性事件:它们的周期以毫秒记(例如,基于采样的性能分析事件)。其他事件仅当事件的持续时间超出阈值时才会触发(例如,读文件事件仅在 read() 超过指定时间时才会触发)。

其他 JFR 事件

JFR 是可扩展的:应用可以定义自己的事件。因此,依据所考虑的应用,你的 JFR 视线可以显示更多类型的事件。例如,WebLogic 应用服务器开启一组应用服务器事件: DBC 操作、HTTP 操作等。这些事件和前面所讨论的 JFR 事件一样:它们可以单个开启,可以有关联的阈值,等等。同样,最新版本的 JVM 可能会添加一些这里没有讨论到的事件。

详情请查阅最新的产品文档。

收集事件自然会引入一些开销。触发事件收集的阈值——因为增加了事件——在开启 JFR 记录时也会增加开销。默认记录中不会收集所有的事件(6 个最耗资源的事件没有开启),基于事件的阈值有些高。这使得默认记录的开销小于 1%。

有时额外的开销是值得的。例如,考虑 TLAB 事件,它可以帮助你判断对象是否正直接分配到老年代,但默认情况下这些事件没有开启记录。同样,默认记录开启了性能分析事件,但只有每 20 毫秒——可以给出适当的概览,但也可能导致采样错误。

JFR 捕获的事件(包括事件的阈值)都定义在模板中(可通过命令行的设置选项选择)。JFR 自带了两个模板:默认模板(限制了事件使得开销效率 1%)和性能分析模板(大多数基于阈值的事件被设置为每 10 毫秒触发)。这个性能分析模板的开销估计是 2%(而你的数据通常也都不一样,我观察到的典型开销要比这小得多)。

模板由 jmc 模板管理器管理。你可能已经注意到图 3-13 中启动模板管理器的按钮了。模板保存在两个地方:$HOME/.jmc/<release> 目录(用户本地)下,和 $JAVA_HOME/jre/lib/jfr(JVM 全局)下。模板管理器允许你选择全局模板(可以说模板“在服务器上”),选择本地模板,或定义新模板。定义模板时,来回多看看可用的事件,依据需要开启(或关闭)它们,还可以选择根据需要来设置事件阈值。

图 3-14 显示 File Read 事件开启,并且阈值为 15 毫秒:读取文件超过该值的就会触发事件。这个事件也可以配置为给 File Read 事件产生栈追踪信息。这增加了开销——这就是为何抓取事件栈追踪信息是可配置的选项。

3.4 Java任务控制 - 图7

图 3-14:JFR 事件模板的一个样本

事件模板是简单的 XML 文件,所以判断模板中哪个事件开启(以及阈值和栈追踪信息配置)的最好办法是阅读 XML 文件。XML 作为配置文件,使得本地模板可以在一台机器上定义,然后复制到全局模板目录下,供团队中的其他人使用。

3.4 Java任务控制 - 图8 快速小结

1.由于 JFR 内建于 JVM,所以可以最大可能地查看 JVM 内部。

2.像其他工具一样,JFR 给应用引入了一些开销。对于日常使用,可以开启 JFR,以较低的开销收集大量信息。

3.JFR 用于性能分析,但它在生产系统中也很有用,所以你可以检查那些导致失败的事件。