JVM性能监控与调优——命令行工具

文章目录

性能问题是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。工欲善其事,必先利其器,想要解决性能相关问题,必须要有比较好的性能诊断工具。Java作为最流行的编程语言之一,应用的性能诊断一直受到业界广泛关注。造成Java应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络I/O、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少,就好比中医、西医看病,中医讲究的是望、闻、问、切,西医则是借助各种检查仪器。

1、概述

JDK本身已经集成了很多诊断工具。在大家刚接触Java学习的时候,最先了解的两个命令就是javac和java,但是除此之外,还有一些其他工具可以使用,可是并非所有的程序员都了解其他命令行程序的作用,接下来我们一起看看其他命令行程序的作用。进入到安装JDK的bin目录,会发现还有一系列辅助工具。这些辅助工具用来获取目标JVM不同方面、不同层次的信息,帮助开发人员很好地解决Java应用程序的一些疑难杂症。Mac系统bin目录的内容如下图所示:

Windows系统bin目录的内容如下图所示:

虽然在Windows系统下都是exe格式的可执行文件。但事实上,它们只是Java程序的一层包装,其真正实现是在tools.jar中,如下图所示。以jps工具为例,在控制台执行jps命令和执行java -classpath %Java_HOME%/lib/tools.jar sun.tools.jps.Jps命令是等价的,即jps.exe只是这个命令的一层包装。下面介绍一些常用的命令工具。

2、jps:查看正在运行的Java进程

jps(JVM Process Status Tool)命令用于查看系统内所有的JVM进程,可根据参数选项指定是否显示JVM的执行主类[包含main()方法的类],以及进程的本地JVMID(Local Virtual Machine Identifier),对于本地JVM进程来说,进程的本地JVMID与操作系统的进程ID是一致的。简单来说,就是Java提供的一个显示当前所有Java进程pid的命令,和Linux系统里的ps命令很相似,ps命令主要是用来显示当前系统的进程情况,比如查看进程列表和进程ID。在日常工作中,此命令也是最常用的命令之一。

jps的基本使用语法如下:

bash 复制代码
     jps  [ options ] [ hostid ]

1、jps命令[options]选项说明:

2、[hostid]说明:

hostid表示目标主机的主机名或IP地址,如果省略该参数,则目标主机为本地主机。如果想要远程监控主机上的Java程序,需要安装jstatd。对于网络安全要求非常严格的场所,需要自定义策略文件来满足对特定的主机或网络的访问,但是这种技术容易受到IP地址欺诈攻击。如果由于安全问题无法通过定制的策略文件处理,那么最安全的操作是在主机本地使用jstat和jps工具。

3、使用案例:

Linux上启动Tomcat(一种Web应用服务器),然后在Linux上面使用ps命令查看Tomcat进程ID使用,如下所示:

bash 复制代码
     ps -ef | grep"tomcat"

运行结果如下图所示:

可以看到进程ID是1224。使用jps -l命令查看,如下图上所示:

3、jstat:查看JVM统计信息

jstat(JVM Statistics Monitoring Tool)用于收集JVM各方面的运行数据,显示本地或远程JVM进程中的类装载、内存、垃圾收集、JIT编译等运行数据。在没有图形用户界面时,只提供了纯文本控制台环境的服务器上,它是运行期定位JVM性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。它的功能非常强大,可以通过它查看堆信息的详细情况。

jstat的基本使用语法如下:

bash 复制代码
     jstat -<option>[-t] [-h<lines>] <vmid>[<interval>[<count>]]

使用下面的命令可以查看jstat相关参数:

bash 复制代码
     jstat -h 或 jstat -help

1、[options]选项说明

2、[-t]参数说明

[-t]参数可以在输出信息前加上一个Timestamp列,显示程序的运行时间。可以比较Java进程的启动时间以及总GC时间(GCT列),或者两次测量的间隔时间以及总GC时间的增量,来得出GC时间占运行时间的比例。如果该比例超过20%,则说明目前堆的压力较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出OOM异常。

3、[-h]参数说明

[-h]参数可以在周期性数据输岀时,输出设定的行数的数据后输出一个表头信息。

4、[interval]参数说明

[interval]参数用于指定输出统计数据的周期,单位为毫秒,简单来说就是查询间隔时间。

5、[count]参数说明

[count]用于指定查询的总次数。

6、使用案列

由于jstat参数选项比较多,这里只列举一个启动了Tomcat的Linux服务器案例,查看其监视状态,命令如下所示。

运行结果如下图所示:

运行结果的各列表示的含义如下表所示:

jstat还可以用来判断是否出现内存泄漏,步骤如下:

  • (1)在长时间运行的Java程序中,可以运行jstat命令连续获取多行性能数据,并取这几行数据中OU列(即已占用的老年代内存)的最小值。
  • (2)每隔一段较长的时间重复一次上述操作,获得多组OU最小值。如果这些值呈上涨趋势,则说明该Java程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。

4、jinfo:实时查看和修改JVM配置参数

jinfo(Configuration Info for Java)可用于查看和调整JVM的配置参数。在很多情况下,Java应用程序不会指定所有的JVM参数。而此时,开发人员可能不知道某一个具体的JVM参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员可以很方便地找到JVM参数的当前值。上面讲解的jps -v命令虽然可以查看JVM启动时显示指定的参数列表,但是如果想要知道未被显示指定的参数的系统默认值,就需要用到jinfo工具了。

第二个作用就是在程序运行时修改部分参数,并使之立即生效。并非所有参数都支持动态修改,只有被标记为manageable的参数可以被实时修改。其实,这个修改能力是极其有限的,使用下面的命令查看被标记为manageable的参数。

bash 复制代码
     java -XX:+PrintFlagsFinal -version | grep manageable

运行结果如下图所示:

jinfo的基本使用语法如下:

bash 复制代码
     jinfo  [ options ] pid

1、[ options ]选项说明

jinfo工具options主要选项如下表所示:

2、pid说明

Java进程ID,必须要加上。

3、使用案例

由于jinfo可以查看JVM配置信息,也可用于调整JVM的配置参数,下面分两类案例分别讲解如何使用。

4、jinfo用于查看JVM配置信息案例

程序很简单,只需要保证程序在执行状态即可。首先使用jps命令查看进程ID,如下所示:

bash 复制代码
     #查看进程ID
     jps -l

执行结果如下:

bash 复制代码
     C:\Users\Administrator>jps -l
     15008 com.yang.ScannerTest
     14820 sun.tools.jps.Jps

上面出现两个结果,ScannerTest程序的进程ID是15008,另外一个进程ID表示jps命令本身的进程。下面使用jinfo命令来查看JVM配置参数,命令如下:

(1)根据进程ID查询全部参数和系统属性:

bash 复制代码
     #查看全部参数和系统属性,
     jinfo  15008

因篇幅所限,只展示部分结果,如下所示:

结果中含有Java系统属性(System Properties)和JVM参数(VM Flags)。

(2)根据进程ID查询系统属性(选项:-sysprops)命令如下:

bash 复制代码
     #查看系统属性
     jinfo -sysprops 15008

运行结果如下,篇幅原因只展示部分结果,如下所示:

通过Java代码获取系统属性如下代码清单所示:

运行结果如下图所示:

通过比较两者结果一致。

(3)查看全部JVM参数配置(选项:flags),命令如下:

bash 复制代码
     #查看全部JVM参数配置
     jinfo -flags 15008

运行结果如下图所示:

可以看到里面包含初始堆大小、最大堆大小等参数配置。

(4)查看某个Java进程的具体参数的值(选项:-flag name),命令如下:

bash 复制代码
     #查看JVM是否使用了ParallelGC垃圾收集器
     jinfo --flag UseParallelGC 15008

运行结果如下图所示:

可以看到结果为-XX:+UseParallelGC,其中UseParallelGC前面的"+"表示已经使用,如果没有使用的话,用"-"表示。也可以查看某个参数的具体数值,比如查看新生代对象晋升到老年代对象的最大年龄,命令如下:

bash 复制代码
     #新生代对象晋升到老年代对象的最大年龄
     jinfo -flag MaxTenuringThreshold 15008

从结果可知新生代对象晋升到老年代对象的最大年龄是15。

5、jinfo用于修改JVM配置信息案例

(1)开启或者关闭对应名称的参数(-flag [±]name)(或者称为修改布尔类型的参数)。

首先查看是否开启输出GC日志的参数,如果GC日志参数是开启状态,那么使用jinfo命令关闭;如果GC日志参数是关闭状态,那么使用jinfo命令开启,命令如下:

对于布尔类型的JVM参数,不仅可以使用-flag [±]name的形式来进行值的改变,也可以使用-flag name=value的形式修改运行时的JVM参数。但是对value赋值必须是1或者0,1表示"+",0表示"-",如下所示:

(2)修改对应名称的参数(-flag name=value)(或者称为修改非布尔类型的参数)。

修改非布尔类型MaxHeapFreeRatio的值,命令如下:

运行结果如下图所示:

除了使用jinfo查看JVM配置参数之外,还有如下方式:

java -XX:+PrintFlagsInitial执行结果如下,展示部分结果:

java -XX:+PrintFlagsFinal执行结果如下,展示部分结果:

输出结果中包含五列。第一列表示参数的数据类型,第二列表示参数名称,第四列表示参数的值,第五列表示参数的类别。第三列"="是参数的默认值,而":="表示参数被用户或者JVM赋值了。可以通过"java -XX:+PrintFlagsFinal |grep":=""命令查看哪些参数是被用户或者JVM赋值的。java -XX:+PrintFlagsInitial只展示了第三列为"="的参数。

java -XX:+PrintCommandLineFlags执行结果如下,该参数输出被用户或者JVM设置过的详细的-XX参数的名称和值:

bash 复制代码
     -XX:InitialHeapSize=268435456
     -XX:MaxHeapSize=4294967296
     -XX:+PrintCommandLineFlags
     -XX:+UseCompressedClassPointers
     -XX:+UseCompressedOops
     -XX:+UseParallelGC

该参数的结果是java -XX:+PrintFlagsFinal的结果中带有":="的部分参数。可以通过该命令快捷地查看修改过的参数。

5、jmap:导出内存映像文件和内存使用情况

jmap(JVM Memory Map)用于生成JVM的内存转储快照,生成heapdump文件且可以查询finalize执行队列,以及Java堆与元空间的一些信息。jmap的作用并不仅仅是为了获取dump文件(堆转储快照文件,二进制文件),它还可以获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。

开发人员可以在控制台中输入命令"jmap-help",查阅jmap工具的具体使用方式和标准选项配置。jmap的基本使用语法如下:

1、[ options ]选项说明

jmap工具[options]主要选项如下表所示:

这些参数和Linux下输入显示的命令多少会有一些不同,也受JDK版本的影响。其中选项-dump、-heap、-histo是开发人员在工作中使用频率较高的指令。

2、使用案例

(1)-dump选项:导出内存映像文件。

一般来说,使用jmap指令生成dump文件的操作算得上是最常用的jmap命令之一,将堆中所有存活对象导出至一个文件之中。执行该命令,JVM会将整个Java堆二进制格式转储到指定filename的文件中。live子选项是可选的,如果指定了live子选项,堆中只有存活的对象会被转储。

通常在写dump文件前会触发一次Full GC,所以dump文件里保存的都是Full GC后留下的对象信息。由于生成dump文件比较耗时,因此大家需要耐心等待,尤其是大内存镜像生成dump文件需要耗费更长的时间来完成。

如果想要浏览dump文件,大家可以使用jhat(Java堆分析工具)读取生成的文件,也可以使用可视化工具进行解读,比如MAT内存分析工具。获取dump文件有手动获取和自动获取两种方式。

手动获取的意思是当发现系统需要优化或者需要解决内存问题时,需要开发者主动执行jmap命令,导出dump文件,手动获取命令如下:

bash 复制代码
     #手动获取堆内存全部信息
     jmap -dump:format=b,file=<filename.hprof><pid>
     #手动获取堆内存存活对象全部信息
     jmap -dump:live,format=b,file=<filename.hprof><pid>

代码清单如下所示,往堆内存中存放数据,然后导出dump文件。

运行程序,最终该程序会产生内存溢出,在执行过程中使用上述命令导出dump文件即可,运行结果如下图所示:

上述命令中,file表示指定文件目录,这里将文件放到D盘根目录,10692表示Java进程ID,结果如下图所示,dump1.hprof就是导出的结果文件。

当程序发生内存溢出退出系统时,一些瞬时信息都随着程序的终止而消失,而重现OOM问题往往比较困难或者耗时。若能在OOM时,自动导出dump文件就显得非常迫切。可以配置JVM参数"-XX:+HeapDumpOnOutOfMemoryError:"使程序发生OOM时,导出应用程序的当前堆快照。

依然使用上面代码,在启动程序之前,在idea中添加如下JVM参数配置:

bash 复制代码
     -Xms60m
     -Xmx60m
     -XX:+HeapDumpOnOutOfMemoryError
     -XX:HeapDumpPath=d:/autoDump.hprof

添加参数如下图所示:

启动程序,运行结果如下图所示:

从结果可知,程序发生了内存溢出,此时dump文件自动生成到目标目录中,如下图所示:

(2)-heap选项:显示堆内存相关信息

命令如下:

bash 复制代码
     #1520表示当前进程ID
     jmap -heap 1520

运行结果如下:

打印heap的概要信息、GC使用的算法、heap的配置和使用情况,可以判断当前堆内存使用情况以及垃圾回收情况。

可以看到最大堆大小为60MB,和前面的VM配置信息一致;新生代大小等于Eden区加From区加To的大小,总共为20MB,符合新生代和老年代比例大小为2:1;老年代大小为40MB,老年代使用率达到了99.53%,说明老年代空间是不足的。

(3)-hiso选项:显示堆中对象的统计信息:

bash 复制代码
     #1520表示当前进程ID
     jmap -histo 1520

运行结果如下,由于篇幅原因,展示部分结果:

上面结果中,instances表示当前的实例数量;bytes表示对象占用的内存大小;classs name表示类名,按照内存大小逆序排列。

(4)-permstat选项:

该选项主要以ClassLoader为口径输出永久代的内存状态信息,仅对Linux和solaris平台有效。

(5)-finalizerinfo选项。

该选项主要用来显示F-Queue中等待Finalize线程执行finalize方法的对象,就是说查看堆积在finalizer队列中的对象。仅对Linux和solaris平台有效。

(6)-F选项

该选项用于当JVM进程对-dump选项没有任何响应时,可使用此选项强制执行生成dump文件。仅对Linux和solaris平台有效。

由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该快照的分析结果存在偏差。例如,假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选项将无法探知到这些对象。另外如果某个线程长时间无法跑到安全点,jmap将一直等待下去。与前面讲的jstat不同,垃圾收集器会主动将jstat所需要的摘要数据保存至固定位置中,而jstat只需要直接读取即可。

6、jhat:JDK自带堆分析工具

jhat(JVM Heap Analysis Tool)命令一般与jmap命令搭配使用,用于分析jmap生成的dump文件(堆转储快照)。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,用户可以在浏览器中查看分析结果。

使用了jhat命令,就启动了一个http服务,端口是7000,即通过访问http://localhost:7000/就可以在浏览器中查看结果。jhat命令在JDK9中已经被删除,官方建议用VisualVM代替。实际工作中一般不会直接在生产服务器使用jhat分析dump文件。

7、jstack:打印JVM中线程快照

jstack(JVM Stack Trace)用于生成JVM指定进程当前时刻的线程快照(Thread Dump),方便用户跟踪JVM堆栈信息。线程快照就是当前JVM内指定进程的每一条线程正在执行的方法堆栈的集合。

生成线程快照的作用是可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题,这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的堆栈情况。

在线程快照中,有下面几种状态,如下表所示:

其中线程的Deadlock、Waiting on condition、Waiting on monitor entry以及Blocked状态需要在分析线程栈的时候重点关注。

jstack的基本使用语法如下:

bash 复制代码
     jstack [ option ] <pid>

1、[ options ]选项说明

jstack工具[options]主要选项如下表所示:

2、使用案例

代码清单如下演示了线程死锁,使用jstack命令观察线程状态。

上面例子很简单,启动了两个线程,分别获取对方的资源,如此造成死锁。下面启动程序,使用jstack命令查看线程状态,命令如下,其中1776是程序的进程ID。

bash 复制代码
     jstack 1776

运行结果如下:

从上面结果中可以发现,Thread-1线程和Thread-0线程互相等待对方的资源,问题代码出现"com.yang.ThreadDeadLock$2.run()"行。在死锁情况出现时,可以很方便地帮助定位到问题。也可以通过Thread.getAllStackTraces()方法获取所有线程的状态,代码清单如下所示:

运行结果如下图所示,可以看到各个线程的状态:

8、jcmd:多功能命令行

在JDK1.7以后,新增了一个命令行工具jcmd。它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能,比如用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等。jcmd拥有jmap的大部分功能,并且官方也推荐使用jcmd命令代替jmap命令。

至于jstat的功能,虽然jcmd复制了jstat的部分代码,并支持通过PerfCounter.print子命令来打印所有的Performance Counter,但是它没有保留jstat的输出格式,也没有重复打印的功能。

jcmd的基本使用语法如下表所示:

9、jstatd:远程主机信息收集

之前的指令只涉及监控本机的Java应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如jps、jstat)。为了启用远程监控,则需要配合使用jstatd工具。

命令jstatd是一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd服务器将本机的Java应用程序信息传递到远程计算机。执行原理如下图所示:

直接打开jstatd服务器可能会抛出访问拒绝异常,这是因为jstatd程序没有足够的权限,如下图所示:

10、小结

介绍了JDK自带的命令行工具及其常用参数。当Java应用和服务出现莫名的卡顿、CPU飙升等问题时,通过JDK自带的状态监控命令和图形化工具,可以非常方便地分析对应进程的JVM状态,进而定位问题并解决问题。

相关推荐
鱼跃鹰飞6 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
王佑辉7 小时前
【jvm】Major GC
jvm
阿维的博客日记7 小时前
jvm学习笔记-轻量级锁内存模型
jvm·cas·轻量级锁
曹申阳10 小时前
2. JVM的架构模型和生命周期
jvm·架构
琪露诺大湿11 小时前
JavaEE-多线程初阶(4)
java·开发语言·jvm·java-ee·基础·1024程序员节·原神
王佑辉14 小时前
【jvm】Full GC
jvm
九鼎科技-Leo14 小时前
C# 内存管理与对象生命周期在面向对象设计中的重要性
jvm·c#
王佑辉1 天前
【jvm】堆空间分代思想
jvm
为啥不能修改昵称啊1 天前
静态数据区,堆,栈
java·jvm·算法
救苦救难韩天尊2 天前
《JVM第7课》堆区
jvm