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 种事件类型。根据发布的版本,事件种类的精确数字会略微有所不同,这里是一些最有用的。
以下每种事件类型的说明都包括两点。事件可以像其他工具如 jconsole 和 jcmd 那样收集基本信息。这类信息在第一点中描述。第二点则描述的是事件提供者,这在 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 来控制。
要开启飞行记录:
% jcmd process_id JFR.start [options_list]
options_list 是一组用逗号分隔的名字 - 值对,控制记录如何进行。可能的选项与命令行使用 -XX:+flightRecorderOptions=string 时的标志完全一样。
如果开启持续记录,可以在任何时间,通过以下命令将当前循环缓冲里的数据转储到文件中:
% jcmd process_id JFR.dump [options_list]
选项列表包括:
name=name
在这个名字下的记录已经开始。
recording=n
JFR 记录的编号。
filename=path
转储文件的位置。
对于给定的进程,可能开启了多个 JFR 记录。以下命令可查看开启的记录:
% jcmd process_id JFR.check [verbose]
这个例子中,用记录启动时的名字来标识它们,也可以任意指定记录的编号(可以在其他 JFR 命令中使用)。 最后是进程放弃记录的命令:
% 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-14:JFR 事件模板的一个样本
事件模板是简单的 XML 文件,所以判断模板中哪个事件开启(以及阈值和栈追踪信息配置)的最好办法是阅读 XML 文件。XML 作为配置文件,使得本地模板可以在一台机器上定义,然后复制到全局模板目录下,供团队中的其他人使用。
快速小结
1.由于 JFR 内建于 JVM,所以可以最大可能地查看 JVM 内部。
2.像其他工具一样,JFR 给应用引入了一些开销。对于日常使用,可以开启 JFR,以较低的开销收集大量信息。
3.JFR 用于性能分析,但它在生产系统中也很有用,所以你可以检查那些导致失败的事件。
