《java性能优化权威指南》---- 第4章:JVM性能监控

本文详细介绍了Java应用的监控重点,包括垃圾收集的各个方面,如垃圾收集器类型、数据指标、日志分析、图形化工具的使用。同时,讨论了JIT编译器的监控,以及类加载的过程和监控方法。通过这些监控手段,可以有效地诊断和优化Java应用的性能。
摘要由CSDN通过智能技术生成

生产环境中应该自始至终地监控应用JVM,JVM是应用软件栈的重要组成部分,应该像监控应用自身和操作系统那样监控JVM,分析JVM监控数据,可以知道何时需要JVM调优。

JVM的监控范围包括垃圾收集、JIT编译以及类加载。

一、垃圾收集

监控JVM的垃圾收集非常重要,因为它对应用的吞吐量和延迟有着深刻的影响。现代JVM,如HotSpot VM,可以将每次GC的数据直接输出成日志文件,以文本方式查看GC统计数据,或者用GUI监控工具查看。

1、重要的垃圾收集数据

重要的垃圾收集数据包括:

  • 当前使用的垃圾收集器;
  • Java堆的大小;
  • 新生代和老年代的大小;
  • 永久代的大小;
  • Minor GC的持续时间;
  • Minor GC的频率;
  • Minor GC的空间回收量;
  • Full GC的持续时间
  • Full GC的频率;
  • 每个并发垃圾收集周期内的空间回收量;
  • 垃圾收集前后Java堆的占用量;
  • 垃圾收集前后新生代和老年代的占用量;
  • 垃圾收集前后永久代的占用量;
  • 是否老年代或永久代的占用触发了Full GC;
  • 应用是否显式调用了System.gc()。

2、垃圾收集报告

HotSpot VM报告垃圾收集数据几乎没有什么额外开销,建议在生产环境中使用。本节列出了各种生成垃圾收集统计数据的命令行选项,并对这些数据进行了解释。

一般来说,垃圾收集分两种:次要垃圾收集(也称为新生代垃圾收集,以下称为Minor GC)和主要垃圾收集(以下称为Full GC)。Minor GC收集新生代, Full GC通常会收集整个堆,包括新生代、老年代和永久代,除了将新生代中的活跃对象提升到老年代之外,还会压缩整理老年代和永久代。因而Full GC之后,新生代为空,老年代和永久代也已压缩整理并且只有活跃对象

开启-xx:+UseParallelGC-XX:+UseParallelOldGC时,如果关闭-XX:-ScavengeBeforeFullGC,HotSpot VM在Full GC之前不会进行MinorGC;如果开启-XX:+ScavengeBeforeFullGC,HotSpot VM在FullGC前会先做一次Minor GC,分担一部分Full GC原本要做的工作,在这两次独立的GC之间Java线程有机会得以运行,从而缩短最大停顿时间,但也拉长了整体的停顿时间。

-XX:+PrintGCDetails

-verbose:gc可能是报告垃圾收集信息最常用的命令行选项,而-XX:+PrintCDetails可以打印更多更有价值的垃圾收集信息。本节以Throughput收集器(通常特指Parallel Scavenge收集器)和CMS收集器为例,解释-XX:+PrintCCDetails的输出日志,也介绍了日志中需要留意的数据模式。

(1)Throughput收集器中查看Minor GC日志

以下是Java 6 Update 25,Throughput收集器(-XX:+UseParallelGC-XX:+UseParallelOldGC)的-XX:+PrintCCDetails输出日志(为便于阅读,已分成多行)。

标签GC说明这是Minor GC,[PSYoungGen: 99952K->14688K (109312K)]是新生代信息。PSYoungGen表示新生代使用的是多线程垃圾收集器Parallel Scavenge。

->”左边的99952K是垃圾收集前新生代的占用量。"->”右边的14688K是垃圾收集后新生代的占用量。新生代进一步分成Eden和2块Survivor,因为Minor GC之后Eden为空,所以此处的14688K也是Survivor的占用量。括号中的109312K不是新生代的占用量,而是Eden和一块正被占用的Survivor的和。

第3行422212K->341136K (764672K)是垃圾收集前后Java堆的使用情况(新生代和老年代的占用量总和),及Java堆的大小(新生代和老年代的总和)。 “->”左边的422212K是垃圾收集前Java堆的占用量, "->"右边的341136K是垃圾收集后Java堆的占用量。括号中的764672K是Java堆的总量。

0.0631991 secs是垃圾收集花费的时间。

通过新生代和Java堆的大小,可以计算老年代大小。例如,Java堆764672K,新生代109312K,老年代则为764672K-109312K = 655360K。

[Times: user=0.06 sys=0.00, real=0.06 secs]是CPU的使用时间。user是垃圾收集执行非操作系统调用指令所耗费的CPU时间。在这个例子中,垃圾收集器使用0.06秒用户态CPU时间。sys是垃圾收集器执行操作系统调用所耗费的CPU时间。本例中,垃圾收集器没有执行操作系统指令,花费的CPU时间为0。real右边是垃圾收集的实际时间(单位秒)。在这个例子中,完成垃圾收集用时0.06秒。user, sys及real的秒数已经四舍五入,并保留小数点后两位。

(2)Throughput收集器中查看Full GC日志

以下是Java 6 Update 25,Throughput收集器(-XX:+UseParallelGC-XX:+UseParallelOldGC)的-XX:+PrintCCDetails输出的Full GC日志(为便于阅读,已分成多行)。
在这里插入图片描述

标签Full GC说明这是完全垃圾收集。[PSYounaGen: 11456K->0K(110400K)]和Minor GC中的含义相同(参见前面的解释)。

[PSOldGen: 651536K->58466K(655360K)]是老年代信息。PSOldGen表示老年代使用的是多线程垃圾收集器Parallel Old。这行“->"左边的651536K是垃圾收集前老年代的占用量。“->"右边的58466K是垃圾收集后老年代的占用量。括号中的655360K是老年代的大小。

662992K->58466K (765760K)是Java堆的使用情况,是垃圾收集前后新生代和老年代占用量的累计。“->”右边可看成是应用在Full GC时的实时数据。了解应用的这此实时数据,特别是应用处于稳定状态时的实时数据,对于调整JVM的Java堆和对JVM垃圾收集器进行精细调优来说,非常重要。

[PSPermGen: 10191K->10191K (22528K)]是永久代信息。PSPermCen表示配合Throughput收集器在永久代使用的是多线程垃圾收集器Parallel Old。这行“->”左边的10191K是垃圾收集前永久代的占用量。“->"右边的10191K是垃圾收集后永久代的占用量。括号中的22528K是永久代的大小。

Full GC中值得重点关注的是垃圾收集之前老年代和永久代的占用量。这是因为,当老年代或永久代的占用接近其容量时,都会触发Full GC。在这个例子中,垃圾收集前老年代的占用(651536K) ,非常接近老年代的大小(655360K)。相比而言,垃圾收集前永久代的占用(10191k),与永久代的大小(22528K)相差较大。因此,这次Full GC是由老年代而引起的。

1.1178951 secs是垃圾收集花费的时间。

[Times: user=1.01 sys=0.00, real=1.12 secs]是CPU的使用时间,含义和之前Minor GC的相同。

(3)CMS收集器中查看Minor GC日志

如果使用并发收集器CMS (-XX:UseConcMarkSweepGC,它自动开启-XX:+UseParNewGc即新生代使用多线程垃圾收集器ParNew ), -XX:+PrintGCDetails日志特别是报告老年代并发垃圾收集情况的数据,会有所不同。

下面是使用并发收集器CMS时的Minor GC日志:
在这里插入图片描述
CMS的Minor GC日志和Throughput收集器是相似的。完整起见,一并解释如下。

标签GC表示这是Minor GC。[ParNew: 2112K->64K(2112K)]是新生代信息。ParNew表示配合CMS收集器在新生代使用的是多线程垃圾收集器ParNew。如果配合CMS使用的是串行垃圾收集器,这里的标签则为DefNew。

ParNew右边,“->"左边的2112K是垃圾收集前新生代的占用量, "->"右边的64K是垃圾收集后新生代的占用量。新生代进一步分为Eden和2块Survivor,因为Minor GC后Eden为空,所以此处的64K也是Survivor的占用量。括号中2112K是新生代的大小,即Eden和一块正被占用的Survivor的和。

0.0837052 secs是新生代回收不可达对象的时间。

下一行16103K->15476K(773376K)是垃圾收集前后Java堆的占用量(新生代和老年代的占用量总和),和Java堆的大小(新生代和老年代的总和)。“->"左边的16103K是垃圾收集前Java堆的占用量,右边的15476K是垃圾收集后Java堆的占用量。括号中的773376K是Java堆的总量。

通过新生代和Java堆的大小,可以计算老年代的大小。例如,Java堆773376K,新生代2112K,老年代则为773376K-2112K =771264K。

0.0838519 secs表示Minor GC的时间,包括新生代垃圾收集、提升对象到老年代以及最后剩余的清扫工作。

[Times: user=0.02 sys-0.00, real-0.08 secs]是CPU的使用时间。user是垃圾收集执行非操作系统调用指令所耗费的CPU时间。sys是垃圾收集器执行操作系统调用所耗费的CPU时间。real右边是垃圾收集的实际用时(单位:秒)。user、sys及real的秒数已经四舍五人,并保留小数点后两位。

(4)CMS收集器中查看Full GC日志

回顾第3章,CMS会在老年代定期执行并发垃圾收集。-XX:+PrintGCDetails可以报告每个并发垃圾收集周期的垃圾收集信息。下面是一个完整并发垃圾收集周期的垃圾收集日志。并发垃圾收集和Minor GC交替出现说明在并发垃圾收集周期中依然可以进行MinorGC。为了便于阅读,日志已经重新格式化,粗体表示并发垃圾收集。
在这里插入图片描述
CMS周期从初始标记开始,到并发重置结束,代码段分别以粗体的CMS-initial-mark和CMSconcurrent-reset表示。CMS-concurrent-mark表示并发标记阶段的结束,CMS-concurrent-sweep表示并发清除阶段的结束,CMS-concurrent-preclean和CMS-concurrent-abortable-preclean用来标识可以并发的工作,为重新标记阶段(标签为CMs-remark )做准备。清除阶段(标签为CMS-concurrent-sweep )释放垃圾对象所占用的空间。CMS-concurrent-reset表示最后阶段,为下一轮并发垃圾收集周期作准备。

日志中特别需要注意的是CMS周期中堆占用的减少量,特别是CMS并发清除开始和结束时(以CMS-concurrent-sweep-start、CMS-concurrent-sweep为标识)Java堆占用的减少量,Java堆的占用量可以查看Minor GC。注意CMS并发清除开始和结束时的Minor GC,如果Java堆的占用几乎没有怎么降低,很少有对象被回收,说明该轮CMS垃圾收集周期几乎没有找到垃圾对象而只是浪费CPU,或者对象以不小于CMS并行清除垃圾对象的速度被提升到老年代。显而易见,这两种情况都说明JVM需要调优,CMS收集器的调优请参阅第7章。

使用CMS收集器时,另一个需要监控的是晋升分布(-xx:+PrintTenuring-Distribution)。晋升分布是一种显示新生代Survivor中对象年龄的直方图。当对象年龄超过”晋升阈值" ( Tenuring Threshold )时,就会被提升到老年代。关于晋升阈值和如何监控晋升分布以及监控它的重要性,将在7.8.6节和7.8.7节中解释。

如果对象提升到老年代的速度太快,而CMS收集器不能保持足够多的可用空间时,就会导致老年代的运行空间不足,这称为并发模式失败(Concurrent Mode Failure )。当老年代碎片化达到某种程度,使得没有足够空间容纳从新生代提升上来的对象时,也会发生并发模式失败。垃圾收集日志(-XX:+PrintGCDetails )中,并发模式失败会有"concurrent mode failure"的字样。发生并发模式失败时,老年代将进行垃圾收集以释放可用空间,同时也会整理压缩以消除碎片。这个操作需要停止所有的Java应用线程,并且需要执行相当长时间。所以,一旦发现并发模式失败,你应该按照第7章,特别是低延迟程序细调一节的指导,对JVM进行调优。

(5)包含时间戳

设置相应的命令行选项, HotSpot VM就可以在日志中包含每次垃圾收集的时间戳。-XX:PrintGCTimestamps在日志中输出自JVM启动以来到垃圾收集之间流逝的秒数。下面是.Throughput收集器结合-XX:+PrintGCTimeStamps-XX:+PrintGCDetails时输出的日志(为便于阅读,已经分成多行):
在这里插入图片描述
注意-XX:+PrintGCTimeStamps日志有时间戳前缀,这个时间是自JVM启动以来的秒数。Full GC的日志也有这样的时间戳前缀。此外,使用CMS收集器时,也会打印这种时间戳。

Java 6 Update 4及更高的版本支持-XX:+PrintGCDateStamps,它生成符合ISO8601标准的时间戳,形如YYYY-MM-DD-T-HH-MM-SS.mmm-TZ。TZ表示时区。

日志中包含时区,时间是该时区的本地时间,而不是GMT时间。下面是吞吐量收集器结合-XX:+PrintGCDateStamps-XX:PrintGCDetails时输出的日志(为便于阅读,已经分成多行)。
在这里插入图片描述

(6)-Xloggc

使用-Xloggc:<filename>可以将垃圾收集的统计数据直接输出到文件(是保存的文件名),以便离线分析。离线分析可以处理时间范围更广的垃圾收集数据,查找问题时也不会直接影响线上应用。结合使用-XX:+PrintGCDetails-Xloggc:<filename>即便不指定-XX:+PrintGCTimeStamps,日志中也会自动添加时间戳前缀,打印格式与-XX:+PrintGCTimeStamps相同。下面是Throughput收集器结合-Xloggc:<filename>-XX:+PrintGCDetails时输出的日志(为便于阅读,已经分成多行)。
在这里插入图片描述

(7)应用停止时间和应用并发时间

使用-XX:+PrintGCApplicationConcurrentTime-XX:+PrintGCApplicationStoppedTime,HotSpot VM可以报告应用在安全点操作之间的运行时间,以及阻塞Java线程的时间。利用这两个命令行选项观察安全点操作有助于理解和量化延迟对JVM的影响,也可以用来辨别是JVM安全点操作还是应用程序引入的延迟。

参见以下示例(-XX:+PrintGCApplicationConcurrentTime-XX:+PrintGCApplicationStoppedTime以及-XX:+PrintGCDetails)

在这里插入图片描述
应用运行大约0.53-0.91秒, Minor GC停顿大约0.045-0.047秒,即Minor GC大约有5%~8%的开销。

注意此处Minor GC之间没有其他的安全点,如果有,则垃圾收集之间的每个安全点都会显示Application time和Total time for which application threads were stopped。

(8)显式垃圾收集

显式的垃圾收集比较容易识别,因为垃圾收集日志中会有特定文字,说明垃圾收集是显式调用System.gc()所引起的。以下是调用System.gc()引发的Full GC日志(-XX:+PrintGCDetails),为便于阅读,已经分成多行。
在这里插入图片描述

Full GC之后的(System)表明是System.gc()导致的Full GC。如果垃圾收集日志中发现该信息,调查其原因,然后决定是否该从源代码中去除System.gc(),或是禁止它。

(9)监控垃圾收集的推荐选项

HotSpot VM监控垃圾收集的最简单命令行选项组合是-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-XX: +PrintGCDateStamps。另一个有用的选项是-Xloggc: <filename>,可以将数据保存为文件以便离线分析。

3、图形化工具

(1)JConsole

在Java 6或更高版本的JVM上开启JConsole,其中<JDK install dir>是JDK的安装目录。

Solaris或Linux上:5

$ <JDK install dir>/bin/jconsole

Windows上:

<JDK install dir>\bin\jconsole

JConsole启动时会自动查找本地进程并弹出连接本地或远程Java应用的对话框。如图:
在这里插入图片描述
监控本地应用,你可以在列表中选择应用的Name和PID,点击Connect按钮。与之相比,远程监控的优点在于,可以将JConsole消耗的系统资源与被监控系统隔离开。为了能远程监控,远程应用需要以开启远程管理的方式启动。开启远程管理需要在远程机器上开启一个与被监控程序通信的端口,如果为了安全选用了SSL,则需要建立密码认证。

需要监控多个Java应用时,可以随时选择菜单Connection > New Connection (新建连接),选择不同的Name和PID。

一旦JConsole连上应用,它会载入6个选项卡。Memory选项卡是最有用的,用于监控JVM垃圾收集。下图显示了JConsole的Memory选项卡。
在这里插入图片描述

Memory选项卡用图表方式显示一段时间内JVM内存的消耗情况。

  • Eden。几乎所有新建的Java对象都在这个内存池。
  • Survivor。该内存池包含那些至少经历过一次Eden垃圾收集而仍然存活的对象。
  • Old或Tenured。该内存池包含那些超过垃圾收集年龄阈值而仍然存活的对象。
  • Metaspace。该内存池包含所有的JVM元数据,例如class和method对象。如果监控的JVM支持类数据共享,这个空间会分成只读和可读写区域。
  • Code cache。HotSpot VM用该内存空间存储经过JIT编译器编译后的代码。
  • Compress class

JConsole将Eden, Survivor及Old (或Tenured )合称堆内存,Permanent,Code cache 和 Compress class合称为非堆内存。

左下角Details详细信息面板显示了当前JVM内存的数据指标,包括如下内容:

  • Used (已使用)。当前已经使用的内存量,包括所有Java对象(可达和不可达对象)占用的空间。
  • Commited (已分配)。保证可供JVM使用的内存量。已分配的内存随着时间推移会发生变化。已分配的内存总是大于或等于已使用的内存。
  • Max (最大值)。内存管理系统可使用的最大内存量。这个值可以更改,也可以不定义。如果JVM已使用的内存逐步增加并超过了已分配的内存,即便没超过最大值(例如系统虚拟内存低时),内存分配也会失败。
  • GC Time (GC时间)。Stop-The-World垃圾收集的累计时间和包括并发回收周期的垃圾收集调用的总数。可能会显示多行,每行代表一个JVM所用的垃圾收集器。

JConsole还有其他监控垃圾收集的功能,参见JConsole文档中的介绍。

(2)VisualVM

VisualVM是开源图形化监控工具,它被认为是第2代JConsole。除去添加了流行工具NetBeans Profiler中的性能分析功能之外,它还集成了一些已有的JDK软件工具和轻量型的内存监控工具,例如JConsole。它也采用NetBeans的插件架构,可以很容易地添加组件或插件,或者扩展VisualVM已有的组件或插件,用来监控和分析应用的性能。

VisualVM中也有性能分析。VisualVM的远程性能分析比较轻量,很适合在监控中进行,本章将介绍这些内容,而性能分析的详细介绍请参见第5章。

在Windows、 Linux或Solaris中,可以用以下命令行启动VisualVM (注意名字是jvisualvm而不是visualvm),其中<JDK install dir>是JDK (JDK 6 Update 6或更高版本)的安装目录。

<JDK install dir>\bin\jvisualvm

对于从java.net下载的VisualVM独立应用,在Windows、 Linux或Solaris上可以用以下命令行启动。其中<Visualvm install dir>是VisualVM的安装目录。

<Visualvm install dir>\bin\visualvm

此外,你也可以从Windows资源管理器导航到VisualVM的安装目录,双击可执行文件,启动VisualVM。

如图所示,启动VisualVM后,左侧为Applications (应用程序),右侧仅有Start Page (起始页),没有监控窗口。
在这里插入图片描述
Application面板上主要有3个可展开的节点。第1个Local (本地)节点,会列出VisualVM可以监控的本地Java应用。第2个Remote (远程)节点,会列出VisualVM可以监控的远程主机,以及这些远程主机上的Java应用。第3个Snapshots (快照)节点,包含一组快照文件。快照可用于捕获应用的重要状态信息或者与其他快照进行比较。

在启动Java应用或VisualVM时, VisualVM会自动识别出本地Java应用。

为了监控远程的Java应用,远程系统必须进行一些配置,运行后台程序jstatd。你可以在jvisualvm和java启动程序的目录下找到jstatd

jstatd会启动Java RMI服务器,监控HotSpot VM的创建和终止,并为远程监控工具诸如VisualVM提供关联( Attach )和监控远程Java应用的接口。运行jstatd必须和运行被监控的Java应用具有相同的用户凭证。因为jstatd可以暴露JVM检测接口( Instrumentation ),所以必须部署安全管理器以及安全策略文件。以下是可供jstad使用的策略文件示例。

grant codebase "file:$(java.home)/../lib/tools.jar" {
	permission java.security.Al1Permission;
};

注意,本示例中的策略文件允许jstad运行时不考虑任何安全异常。这个策略比授予全部权限给所有的codebase要多些限制,但比运行jstatd服务所需要的最小权限要宽松。可以指定比本例更严格的安全策略,从而进一步限制访问。但是,如果安全策略文件无法解决安全问题,最保险的方法是不运行istatd,采用本地监控工具而不是远程连接

假定上述策略被保存为名叫jstatd.policy的文件,可以运行以下命令启用该策略并启动jstatd

jstatd -J-Djava.security.policy=<path to policy file>/jstatd. policy

远程系统运行jstatd后,你可以在本地系统运行jps加远程系统的主机名,验证能否关联远程的jstatdjps可以列出能被监控的Java应用。如果jps带主机名参数,它会尝试连接远程系统的jstatd,查找远程可被监控的Java应用。如果jps没有带主机名参数,则返回本地能被监控的Java应用。

假设远程系统已经配置好,运行jstatd的主机叫halas,在本地系统,你可以执行以下jps命令来验证到远程系统的连通性。

$ jps halas 
2622 Jstatd

jps返回jstatd,说明远程系统上的jstatd已经配置成功了。Jstatd前的数字是jstatd的进程id,对于验证远程系统的连通性来说,进程id具体是什么并不重要。

使用VisualVM监控远程Java应用,需要配置远程主机名或IP地址,可以在VisualVMApplication面板上右键Remote节点,添加远程主机信息。如果你想监控多个远程主机上的Javar用,则必须在每台远程主机上用之前的方法配置jstatd,然后在VisualVM中添加主机信息。VisualVM以自动发现并列出可被监控的Java应用。再次回顾一下,运行远程Java应用的用户凭证必须和运行VisualVM的相匹配,同时jstatd的权限需要和策略文件相吻合。

可以双击Local或Remote节点下的应用名或图标来监控某个应用,还可以右键选择应用名或图标,选择Open。这两种操作都会在VisualVM的右侧面板中打开一个选项窗口。如图:
在这里插入图片描述
如果远程应用配置了JMX连接,你也可以从Monitor窗口发起垃圾收集或者转储堆的请求。远程应用配置JMX,启动时至少需要以下系统属性:

com.sun.management.jmxremote.port=<port number>
com.sun.management.jmxremote.ssl=<true | false>
com.sun.management.imxremote.authenticate=<true | false>

配置VisualVM通过JMX连接远程应用,可以选择菜单File>Add JMX Connection (添加JMX连接),在Add JMX Connection窗口的输入框中添加必要的信息。如果你设置com.sun.management.jmxremote. authenticate=true,则需要在用户名和口令框中输入授权远程连接的用户名和口令。

配置完JMX连接后,VisualVM的Applications面板会新增一个图标,表示远程应用的JMX连接已经配置好。在VisualVM中配置远程应用的JMX连接可以增加监控功能。比如,Monitor可以显示应用的CPU使用率,也可以触发Full GC和Heap Dump (堆转储),如图所示。
在这里插入图片描述
Threads窗口可以深入了解线程,哪些最活跃、哪些在获取和释放锁之间徘徊。所有的本地应用都有Threads窗口这个视图。在观察应用特定的线程运行行为,特别是当操作系统的监控表明应用可能面临锁竞争时,Threads窗口会很有用。

可以在Threads窗口中点击Thread Dump (线程转储)创建一个线程转储,VisualVM会增加个窗口显示这些线程转储,同时会在Applications的该应用节点之下新增一个线程转储的节点。需要注意的是,除非保存线程转储,否则VisualVM关闭后这此线程转储就没有了。在应用下面的线程转储图标或标签上右键选择save as (另存为)就可以将其保存。之后,可以选择菜单FileLoad(装入)重新加载线程转储。

VisualVM也为本地和远程应用提供了性能分析。监控应用在运行时的CPU或内存使用情况会很有用。然而,如果在生产环境使用这些特性,就需要多加小心了,因为它们可能会加重应用的负担。

远程性能分析需要配置JMX连接,而且只能进行CPU性能分析而不包括内存分析。不过可以从Sampler (抽样器)窗口生成堆转储,,Threads窗口也能生成。堆转储可以载入VisualVM进行内存分析。

远程性能分析的第一步是在Applications面板中打开一个配置好JMX连接的远程应用。然后选择右侧面板中的Sampler,点击CPU,开始远程性能分析。下图显示了点击CPU按钮之后的Sampler窗口。
在这里插入图片描述
点击Pause (停止)可以停止和恢复性能分析,点击Snapshot以捕获快照。拍过后, VisualVM会显示快照。快照可以保存。如下图所示,你还可以导出快照文件,与其他开发者分享、再次导人或者与其他快照比较。快照也可以在VisualVM或NetBeans Profiler中导人。你可以从VisualVM主菜单选择FileLoad,选择Profile Snapshot ( Profiler快照) (*.nps )文件然后装入。

在这里插入图片描述
快照窗口中的Call Tree (调用树)显示快照中所有线程的调用栈。点开树节点之后可以查看调用栈以及耗时和CPU最多的方法。快照窗口的底部,你可以查看Hot Spot (热点),它是一个方法列表,按照方法消耗Self Time (自用时间)的多少排序。此外Combined (组合)视图综合显示了Call Tree和Hot Spot,点击Call Tree中的调用栈, Hot Spot method (热点-方法)表就会刷新只显示所选调用栈中的方法。

更多Java应用性能分析的内容请参考第5章。

VisualVM也可以载入jmapJConsole或发生OutOfMemoryError (-XX: +HeapDumpOnOutOfMemoryError)时自动生成的二进制堆转储文件。二进制堆转储是堆转储时JVM堆中所有对象的快照。Java 6中,你可以用jmap-dump:format=b,file=<fil ename> <jvm pid>生成二进制堆转储,其中<filename>是二进制堆转储文件的文件名(含路径),<jvm pid>是JVM的进程名。Java 6的JConsole也可以用HotSpotDiagnostics MBean生成堆转储。生成的二进制堆转储,可以在VisualVM中通过File>Load菜单载入,然后进行分析。

(3)VisualGC

VisualGC是VisualVM的插件,它可以监控垃圾收集、类加载和JT编译。插件版VisualGC比独立版的优势在于,VisualVM会自动发现和显示可被监控的JVM。

监控本地应用时,VisualGC必须和应用程序使用相同的用户凭证。监控远程应用时, jstatd也必须和被监控的Java应用使用相同的用户凭证。如何配置和运行jstatd请参考前面"VisualVM"小节。

如果VisualVM已经添加VisualGC,在你监控Applications面板中的应用时,右边就会显示增加的VisualGC窗口页。依据所用的垃圾收集器,VisualGC可以显示2个或3个面板。使用Throughput收集器时,显示两个面板: Spaces (区域)和Graphs (图表);使用CMS或Serial收集器时,Spaces和Graphs下面会显示第3个面板Histogram (直方图),下图显示VisualGC的所有面板。

  • Spaces面板
    以图形化方式展示垃圾收集空间以及它们的使用情况。面板垂直分成3块, Perm(永久代)、Old (或Tenured,老年代)及新生代(包括Eden和两个Survivor, So和S1 ),这些垃圾收集空间显示区域的大小与JVM分配给它的最大值成比例,每个区域都用不同于其他区域的颜色表示相对于该空间最大容量的当前使用量。在Graphs和Histogram面板中,与Spaces中颜色相同的表示同一个垃圾收集空间。

    Throughput收集器(-xx:+UseParalle1CC-xx: +UseParal l e101dCC )默认时采用自适应的尺寸调整策略,新生代几个空间之间的关系或比率随着时间会发生变动。启用自适应尺寸调整策略时, Survivor的大小会发生变化,新生代的3个区之间也会动态重新划分空间。此时,屏幕上的Survivor以及代表已使用空间的着色区域,都是相对于新生代当前空间(而非空间最大值)的相对尺寸。当JVM自适应调整新生代大小时,与之关联的屏幕区域也会随之刷新。

    Eden中颜色块的每轮上升和下降都表示一次Minor GC。上升到下降的周期速率表示Minor GC的频率。而更重要的是,你该关注Survivor溢出。观察Minor GC时Survivor的占用情况,可以找出Survivor溢出。如果你发现每次Minor GC后Survivor满或者接近满,并且老年代的使用在增长,说明Survivor可能发生溢出。不过一般来说,这种现象表明对象正在从新生代提升到老年代。如果提升太早或太快,最终会导致Full GC。当Full GC时,你可以看到老年代的使用量在下降。老年代使用量的下降频率说明Full GC的频率。

  • Graphs面板
    它把性能统计数据以时间函数的方式水平展示。该面板显示垃圾收集、JIT编译和类加载。后两种本章将稍后讨论。x轴的精度取决于Spaces上方的Refresh Rate (刷新频率),Graphs水平视图的每次采样占用屏幕两个像素。显示的高度取决于包含多少统计数据。

    Graphs面板显示以下内容:

    • Compile Time (编译时间)
    • Class Loader Time (类加载时间)
    • GC时间(GC Time)
    • Eden Space (Eden区)
    • Survivor 0/Survivor 1 (Survivor 0和Suvivor 1)
    • Old Gen (老年代)
    • Perm Gen (永久代):JDK7显示的是Matespace
  • Histogram面板
    当使用CMS或Serial收集器时,Histogram会显示在Spaces和Giraphs下。由于Throughput收集器使用其他机制维护Survivor中的对象,所以不会维护Survivor中对象的年龄。因此被监控的JVM使用收集器是地,不会显示直方图。

    Histogram显示存活对象及其年龄的数据,它有Parameters和Histogram两个子面板。Parameters子面板显示Survivor区的当前大小,以及对象从新生代提升到老年代的控制参数。晋升阈值在Parameters面板显示为Tenuring ThresholdParameters显示的最大Tenuring國值,是一个对象能够保留在Survivor中的最大年龄。对象从新生代被提升到老年代,基于晋升國值而不是最大晋升阈值

    如果经常发生晋升阈值小于最大晋升阈值,说明对象从新生代提升到老年的速度太快。这通常是因为Survivor溢出。一旦溢出,最老的对象就被提升到老年代,直到Survivor的使用不再超过Parameters上的Desired Survivor Size。如前所述,Survivor溢出会填满老年代而导致Full GC。

    Histogram子面板显示最近一次Minor GC之后活动的Survivor中对象年龄分布的快照。如果监控Java 5 Update 6或更高版本的JVM,这个面板则会包括16个相同尺寸的区域,每一个对应一种可能的对象年龄。每个区域表示活动Survivor的100%空间,填充的颜色区域表示指定年龄的对象占Survivor区的百分比。

    在应用运行时,你可以看到长时间存活的对象在各个年龄区中穿越。长时间存活对象占的空间越大,在年龄区之间迁移的光点就越大。当晋升阈值小于最大值时,你可以看到比晋升阈值大的区域没有被使用,因为这些对象已经被提升到老年代了。

二、JIT编译器

当你想找出哪些方法被优化,或某些情况下的逆优化( Deoptimized )或重新优化( Reoptimized )时,监控JIT编译就有用了。JIT编译器优化时会有一些初始假设,如果之后发现不正确,就可能会发生逆优化或者重新优化。在这种情况下,JIT编译器会放弃之前所做的优化而基于获得的新信息重新优化。

可以使用-XX:+PrintCompilation监控HotSpot JIT编译器。-XX:+PrintCompilation为每次编译生成一行日志。日志样例如下:
在这里插入图片描述
图形工具VisualGC上Graphs面板的Compile Time (编译时间,见图)可能最有用了,因为它以脉冲方式显示JIT编译活动。VisualGC上Graphs面板可以清晰显示JIT编译活动。
在这里插入图片描述

VisualGC上Graphs面板的Compile Time显示了编译所化的时间。面板高度与任何值都无关。一个脉冲表示一次JT编译。窄脉冲表示持续时间相对短,宽脉冲表示活动持续时间长。没有脉冲,表示不在编译。标题栏显示JIT编译的总数以及编译累计的时间。

三、类加载

许多应用都会使用自定义的类加载器,有时称为用户类加载器。JVM类加载器负责加载类,也负责卸载类。何时加载或卸载类取决于JVM运行时环境和所用的类加载器。监视类加载活动是有价值的,特别是在应用使用自定义类加载器的时候。至本文写作时为止,HotSpot VM会把所有类的元数据信息都加载到永久代。当永久代满时,就会发生垃圾收集。因此,监视类加载活动和永久代的使用,对于应用性能能否满足需求是有重要意义的,另外垃圾收集的统计数据也可以指明类是何时从永久代卸载的。

当需要加载其他类而空间不足时,未使用的类就会从永久代中被卸载从永久代卸载类,意味着需要Full GC,而程序可能会因此遭遇性能问题。下面日志显示的是Full GC时有类被卸载。
在这里插入图片描述
垃圾收集日志显示有4个类被卸载。Full GC过程中的类卸载,说明永久代需要扩大,或者它的初始大小需要扩大,你应该用命令行选项-XX:PermSize-XX:MaxPermSize调整永久代的大小。为避免FullGC扩大或缩小永久代的可分配空间,可以设置--XX:PermSize-XX:MaxPermSize为相同值。注意,如果永久代开启并发垃圾收集,你可能会在永久代并发垃圾收集周期中看到类被卸载。永久代并发垃圾收集周期不是Stop-The-World,所以应用不会感受到垃圾收集导致的停顿。并发永久代垃圾收集只能和并发收集器CMS一起使用

其他永久代调优的指南和技巧,包括如何开启永久代的并发垃圾收集,可以参考第7章。

图形工具JConsole, VisualVM和VisualGC插件可以监视类加载。下图中JConsole的Classes (类)选项卡,显示了当前已加载类的数量,已卸载类的总数,以及已加载类的总数。
在这里插入图片描述
VisualVM选项卡Monitor中的Classes也可以监视类加载。它显示已加载类的总数和已加载共享类的总数。可以在这个视图上确认被监视的JVM是否开启了类数据共享。类数据共享是指在同一个系统内的JVM之间共享类、减少内存占用的特性。如果监视的JVM使用类共享,横向上除了根线显示已加载类的总数之外,还有一根线显示已加载共享类的总数,如图所示。

在这里插入图片描述
你也可以在VisualGC的Graphs窗口中查看Class Loader (类加载器)面板以监视类加载,如图所示。
在这里插入图片描述
Class Loader面板上的脉冲表示类加载或者卸载。窄脉冲表示类加载持续的时间短,宽脉冲表示类加载持续的时间长。没有脉冲表示没有类加载。标题显示已经加载类的数量,已卸载类的数量以及自程序启动以来累计的类加载时间。如果发现Class Loader面板和下方GC Time (GC时间)面板有正对的脉冲,说明(类加载)同时也有垃圾收集活动,这可能是因为JVM永久代正在进行垃圾收集。

四、Java应用监控

监控应用的常用方法是查看日志,日志中包含了重要的事件和应用性能的指示信息。有些应用内建MBean,可以通过Java SE监控管理API进行监控和管理。可以用兼容JMX的工具来查看和监控这些MBean,例如JConsole或VisualVM的VisualVM-MBeans插件(可以在VisualVM Tools>Pluging的插件中心找到它)。

图4-20中VisualVM-MBeans插件只显示了其中一部分。

TODO mbean 的图

1、快速监控锁竞争

快速定位Java应用中的锁竞争,笔者常用的技巧是用JDK的jstack抓取线程转储信息。如果你的目标主要是监控(目的是快速获取数据)而不是建立、配置性能分析器以便详细分析,这个方法就很管用。

下面是某个应用的jstack日志,它的读写线程共享一个队列。写线程往队列中写入数据,读线程从中读取数据。

本例中只包括相关的栈追踪信息,用于演示如何使用jstack快速查找锁竞争。jstack日志中,线程Read Thread-33已经成功获取共享的队列锁(即Queue对象,地址0x22e88b10,用粗体·字表示,如locked <0x22e88b10> (a Queue) )。

其他线程的栈追踪信息显示它们都在等待Read Thread-33持有的锁,见图中的粗体字waiting to lock <0x22e88b10> (a Queue).
在这里插入图片描述
特别需要注意的是,本例中的锁地址是相同的(尖括号 中的十六进制数),锁地址是锁在jstack日志中的唯一标识。如果栈追踪信息中的锁地址不同,说明是不同的锁,换句话说,锁地址不同的线程不是在竞争同一个锁。

jstack日志中查找锁竞争的关键在于,从多个线程的栈追踪信息中查找相同的锁地址,然后找到等待该锁地址的线程。如果发现多个线程的栈追踪信息都试图锁住相同的锁地址,说明应用正面临锁竞争。抓取多份jstack日志,如果在同一个锁上一直出现类似的锁竞争,那么应用极有可能正面临高度锁竞争问题。注意,栈追踪信息提供了发生锁竞争的代码在源代码中位置。从Java应用的源代码中找到发生高度锁竞争的位置历来是件困难的事,用上面介绍的jstack方法追踪应用中的锁竞争非常有用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值