虚拟机性能监控与故障处理
1.基础故障处理工具
1.1jps:虚拟机进程状况工具
JDK的很多小工具的名字都参考了UNIX命令的命名方式,jps(JVM Process Status Tool
)是其中的典型。除了名字像UNIX的ps命令之外,它的功能也和ps命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)
。虽然功能比较单一,但它绝对是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID,Process Identifier)是一致的,使用Windows的任务管理器或者UNIX的ps命令也可以查询到虚拟机进程的LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就必须依赖jps命令显示主类的功能才能区分了。
测试jps命令的使用样例:
1 | public class ScannerTest { |
1.2jstat:虚拟机统计信息监控工具
jstat(JVM Statistics Monitoring Tool
)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载
、内存
、垃圾收集
、即时编译
等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。
测试jstat命令的使用样例:
1 | /** |
选项option代表用户希望查询的虚拟机信息,主要分为三类:类加载
、垃圾收集
、运行期编译状况
。
类加载相关信息
垃圾收集相关信息
运行期编译状况相关信息
使用jstat工具在纯文本状态下监视虚拟机状态的变化,在用户体验上也许不如后文将会提到的JMC
、VisualVM
等可视化的监视工具直接以图表展现那样直观,但在实际生产环境中不一定可以使用图形界面,而且多数服务器管理员也都已经习惯了在文本控制台工作,直接在控制台中使用jstat命令依然是一种常用的监控方式。
1.3jinfo:Java配置信息工具
jinfo(Configuration Info for Java
)的作用是实时查看和调整虚拟机各项参数。使用jps命令的-v
参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用jinfo的-flag
选项进行查询了(如果只限于JDK 6或以上版本的话,使用-XX:+PrintFlagsFinal
查看参数默认值也是一个很好的选择)。
执行样例:查询CMSInitiatingOccupancyFraction
参数值:
1 | /** |
1.4jmap:Java内存映像工具
jmap(Memory Map for Java
)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不使用jmap命令,要想获取Java堆转储快照也还有一些比较“暴力”的手段:譬如-XX:+HeapDumpOnOutOfMemoryError
参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件,通过-XX:+HeapDumpOnCtrlBreak
参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。
jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
执行样例:手动利用jmap -dump:live,format=b,file=heap.bin <pid>
导出堆转储文件的方式:
1 | /** |
执行样例:利用-XX:+HeapDumpOnOutofMemoryError -XX:HeapDumpPath:C:\Users\34404\oom.hprof
自动导出堆转储文件的方式:
执行样例:利用jmap -histo <pid>
显示堆中对象统计信息:
由于jmap需要访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助
安全点机制
,让所有的线程停留在不改变堆中对象引用的状态。也就是说,由jmap导出的堆转储文件必定是安全点位置的,这可能导致基于该dump文件的分析结果存在偏差,例如假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选型将无法探知到这些对象。另外,如果某个线程长时间无法跑到安全点,jmap会一直等下去,与jmap不同,垃圾收集器会主动将jstat所需要的统计信息保存在固定位置供jstat直接读取。
1.5jhat:虚拟机堆转储快照分析工具
JDK提供jhat(JVM Heap Analysis Tool
)命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/Web服务器
,生成堆转储快照的分析结果后,可以在浏览器中查看。不过实事求是地说,在实际工作中,除非手上真的没有别的工具可用,否则多数人是不会直接使用jhat命令来分析堆转储快照文件的,主要原因有两个方面。
- 一是一般不会在部署应用程序的服务器上直接分析堆转储快照,即使可以这样做,也会尽量将堆转储快照文件复制到其他机器上进行分析,因为
分析工作是一个耗时而且极为耗费硬件资源的过程
,既然都要在其他机器上进行,就没有必要再受命令行工具的限制了。 - 另外一个原因是jhat的分析功能相对来说比较简陋,后文将会介绍到的
VisualVM
,以及专业用于分析堆转储快照文件的Eclipse Memory Analyzer
等工具,都能实现比jhat更强大专业的分析功能。
分析结果默认以包为单位进行分组显示,分析内存泄漏问题主要会使用到其中的“Heap Histogram”(与jmap -histo
功能一样)与OQL页签的功能,前者可以找到内存中总容量最大的对象,后者是标准的对象查询语言,使用类似SQL的语法对内存中的对象进行查询统计。
1.6jstack:Java堆栈跟踪工具
jstack(Stack Trace for Java
)命令用于生成虚拟机当前时刻的线程快照
(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因
,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
执行样例:利用jstack
命令检查线程间死锁问题:
1 | public class JstackTest { |
从JDK 5起,java.lang.Thread类新增了一个getAllStackTraces()
方法用于获取虚拟机中所有线程的StackTraceElement
对象。使用这个方法可以通过简单的几行代码完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈:
1 | public class JavaAPITest { |
2.可视化故障处理工具
2.1jconsole:Java监视与管理控制台
JConsole(Java Monitoring and Management Console
)是一款基于JMX(Java Management Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整。JMX是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以运行于虚拟机之上的软件中,典型的如中间件大多也基于JMX来实现管理与监控。虚拟机对JMX MBean的访问也是完全开放的,可以使用代码调用API、支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问。
1 | /** |
1 | public class ThreadDeadLockTest { |
2.2VisualVM:多合一故障处理工具
VisualVM(All-in-One Java Troubleshooting Tool
)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是Oracle官方主力发展的虚拟机故障处理工具。Oracle曾在VisualVM的软件说明中写上了“All-in-One”的字样,预示着它除了常规的运行监视、故障处理外,还将提供其他方面的能力,譬如性能分析(Profiling)。VisualVM的性能分析功能比起JProfiler、YourKit等专业且收费的Profiling工具都不遑多让。而且相比这些第三方工具,VisualVM还有一个很大的优点:不需要被监视的程序基于特殊Agent去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中
。这个优点是JProfiler、YourKit等工具无法与之媲美的。
1 | /** |
2.3Memory Analyzer Tool:堆转储文件分析工具
1 | /** |
3.JVM调优案例分析与实战
3.1内存泄漏问题分析与排查
Java中内存泄漏的8种常见情况:
静态集合类
:如HashMap、ArrayList等,如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束前不能被释放,从而造成内存泄漏。简而言之,长生命周期的对象持有短生命周期对象的引用
,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
1 | public class MemoryLeak { |
单例模式
:与静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期与JVM的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,从而造成内存泄漏。内部类持有外部类
:如果一个外部类的实例对象的方法返回了一个内部类的实例对象,并且该内部类对象被长期引用,那么即使外部类对象不再使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,从而造成内存泄漏。连接对象如数据库连接、网络连接、IO连接等
:在对数据库进行操作时,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放数据库连接,否则如果不显式关闭Connection、Statement或ResultSet等,将会造成大量的对象无法被回收,从而造成内存泄漏。变量不合理的作用域
:一般而言,一个变量定义的作用域大于其使用范围,很有可能会造成内存泄漏,另一方面,如果没有及时把对象设置为null,也很有可能导致内存泄漏的发生。
1 | public class MemoryLeak { |
上面的代码中通过readFromNetwork方法把接受的数据保存到变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经没用了,但是由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此导致内存泄漏。实际上,这个msg变量可以定义在readFromNetwork方法内部,当方法使用完毕,msg的生命周期也结束了,此时就可以回收了。还有一种方法,在使用完msg后,把msg置为null,这样也能被回收。
改变哈希值
:当一个对象放入HashSet中以后,就不能修改这个对象的参与哈希值计算的字段了,否则后续无法找到,导致无法删除,从而造成内存泄漏。缓存泄漏
:可以使用WeakHashMap表示缓存,当没有其他外部强引用时,GC发生时可以回收掉缓存数据。监视器和回调
:如果客户端在你实现的API中注册回调,却没有显式取消,那么就会累积。
内存泄漏的案例:
1 | public class Stack { |