写点什么

JAVA 应用生产问题排查步骤

  • 2021 年 11 月 24 日
  • 本文字数:7479 字

    阅读完需:约 25 分钟

JAVA 应用生产问题排查步骤

学会这篇文章里面的命令并熟练使用,出去面试就可以说自己有 5 年工作经验并且精通 JVM 了。本篇文章中介绍的命令绝对是 JAVA 程序员平时工作中经常使用的并且必须会的命令,如果你不会你就是没有工作经验的人。如果你不会,那么在别人眼中你肯定就是菜鸟一个,所以本篇文章中的命令,你必须学会并且熟练使用。

注意:本篇文章内容基于 JDK 版本:

java version “1.6.0_113”

Java™ SE Runtime Environment (build 1.6.0_113-b01)

Java HotSpot™ 64-Bit Server VM (build 20.111-b01, mixed mode)

1 JAVA 自带命令 JPS

jps 这个命令在 JDK 的安装目录 bin/下面


使用 jps 这个命令可以找到运行在服务器(电脑)上面的所有 JVM 进程。jps 最常用、最有用的命令有以下俩个。

  • jps -vljps -vl 这个命令中的-v 是输出启动 JVM 进程时传递给 JVM 的参数,-l 是输出 main 方法所在类的完整路径或者 JAR 包的完成路径,命令结果如下截图:

  • jps -mljps -ml 这个命令中的-m 是输出传给 main 方法的参数,-l 是输出 main 方法所在类的完整路径或者 JAR 包的完成路径,命令结果如下截图:在 Liunx 服务器上面执行 jps -ml 结果如下截图:工作中最常用的命令是 jps -vl。jps -ml 这个命令结果比较鸡肋并且不常用。你可以自己写一个 main 方法,在 main 方法里面写一个死循环,并且传递给 main 方法几个参数,然后再执行一下 jps -vl 和 jps -ml 这俩个命令感受一下。你如果光看我的文章,自己不手敲这些命令,你就是瞎看,没一点用。

如果你们公司是通过 weblogic 部署的应用,那么 weblogic 启动的时候会给 JVM 进程传一个参数-Dweblogic.Name= yourwebname ,这个 yourwebname 就是你部署应用的应用名称,然后你可以通过 jps -vl |grep yourwebname 这个命令找到你 javaweb 项目的 jvm 进程 id。

但是,如果你的应用是通过 tomcat 部署,那么 jps -vl |grep yourwenname 这个命令就不太管用了。不过 jps -vl 这个命令肯定还是管用的,因为 jps 是 java 自带的命令,跟 tomcat 或者 weblogic 没有任何关系。

如果你的应用部署在 tomcat 上面,那么你就用 jps -vl 找 tomcat 这个 JVM 进程就可以了。原理如下:

一个 tomcat 就只能启动一个 JVM 进程,如果你的 tomcat 里面部署了多个 Java Web 应用, 那么你的多个 Java Web 应用共享这个一个 JVM,每个 JavaWeb 应用都有自己的 ClassLoader,也就是说一个 JVM 中可以有多个 Class Loader。

如果其中一个 JavaWeb 应用发生了内存泄漏或者别的原因把 JVM 搞垮了,那么这个 tomcat 下面的所有 JavaWeb 应用就全挂了,所以生产环境没有人会用一个 tomcat 去部署多个 JavaWeb 应用的。生产环境,都是启动多个 tomcat,每个 tomcat 只部署一个 JavaWeb 应用。

我感觉一个 tomcat 只能启动一个 JVM 进程这个设计非常不合理,假如我一个 tomcat 下面部署了多个 JavaWeb 应用,我想给每个 JavaWeb 应用设置不同的 JVM 参数都不行。现在的 SpringBoot 应用都是直接通过 java -jar app.jar 启动的,并且 SpringBoot 直接就内置了一个 tomcat,所以你通过 java -jar app.jar 启动的时候就是一个 tomcat 只部署一个 We 应用。

关于 tomcat 的知识我是参考 CSDN 网站上面的大神 greencacti 的文章: Tomcat 中会启动一个 jvm 还是多个 jvm? 。

还有 CSDN 网站上面的大神 tyyh08 的文章

一个 tomcat 部署多个应用,有几个 JVM? 。

2 Liunx 上面 top 命令

使用 jps 命令找到我们的 jvm 进程 ID 之后赶紧使用 top 命令看一下服务器上面的大致情况,top 命令默认好像是 3 秒钟刷新一次。

top 命令结果如下截图:


top 命令结果显示之后按小写字母 c,可以显示完整的命令行,这个非常有用,一定要用。如截图


然后按大写母 P,以 CPU 使用率排序(按进程负载排序,排在最上面的就是 CPU 使用率最高的)


按大写字母 M,以内存使用率排序


高亮显示:可以这样搞,先按大写字母 P,再按小写字母 b,再按小写字母 x。效果如下截图:


按键 b 打开或关闭 运行中进程的高亮效果

按键 x 打开或关闭 排序列的高亮效果

按数字 1 键,可以显示多个 CPU 的使用情况


top 显示的是服务器上面所有的进程概况,top 类似于 Windows 电脑上面的的任务管理器。


但是,如果我只关心我自己的 JVM 进程呢?可以使用这个命令

top -p jvm 进程 ID,这个命令可以指定进程,只显示指定进程的概况,按空格键可以立即刷新。


top -d 2 -p jvm 进程 ID,这个命令可以指定进程,只显示指定进程的概况,-d 2 的意思是俩秒钟刷新一次。


top -Hp 进程 ID,这个命令可以显示指定进程下面的线程信息, 这个超级有用,必须要会。 而且这个命令搭配 JAVA 的自带命令 jstack 可以非常快速并且有效的定位代码问题。


然后使用 Linux 的自带命令,printf 将线程 ID 转换成 16 进制,printf “0x%x\n” 19235


然后使用 JAVA 的自带命令 jstack 去找到这个线程 ID 都在干什么就行了。这几个命令非常非常重要。

使用 top 命令,其实主要关注 top 命令里面的 RES 列的值,%CPU 列的值,%MEM 列的值,这三列的值就行了。然后

拿 RES 列的值跟后面 jmap 命令显示的 jvm 堆的值做比较。如果 RES 的值,比你的 Xmx 的值还大, 注意是比你设置的 Xmx 的值还大 ,说明你的 java 程序引用的有非堆(堆外地址)内存,比如 NIO,DirectByteBuffer 这些类会使用堆外的内存。所以要注意堆外内存泄露情况(就是你代码里面虽然已经不用这块堆外地址了,但是你的引用没释放,导致你的程序浪费了很多用不到的堆外地址)。 堆外内存泄漏可以使用 Google 出品的 perf 工具来排查。perf 工具使用参见,大神的文章 feininan 的文章《 使用 google perf 工具来排查堆外内存占用 》。

这块知识点摘自网络上面 MartinDai 大神的文章 记一次堆外内存泄漏排查过程 。 执行 top 命令,再按 c,看到对应的进程所占用的 RES 有 8 个多 G(这里当时忘记截图了),但是实际上我们配置的 Xmx 只有 3G,而且程序还是正常运行的,所以不会是堆占用了这么多,于是就把问题方向指向了非堆的内存。

RES 列和 %MEM 列的关系为:RES/总内存=%MEM,我们来算一下:


从上图可以看到,这台服务器的物理内存(运存)为:132024628k = 126G.

32924 这个进程的 RES 列的值为:5.3G

32924 这个进程的 %MEM 列的值为:4.2%

我们来算一下:RES:5.3G / total Mem:126G = 0.042 = 4.2%

这下你明白了吧 RES 列和 %MEM 列,表达的其实是一个意思。

RES 的意思是:Resident Memory Size 常驻内存大小。使用 man top 命令看下 top 命令的官方帮助文档怎么说:




TIPS:按小写字母 q 可以退出 top 命令

JVM 进程的 RES 列的值和 JVM 的 heap(堆)的关系也很有意思,RES 列的值代表 JVM 进程在运行过程中曾经使用过的最大内存, 注意是曾经使用过的最大内存 ,并不代表此时此刻 JVM 堆使用的内存大小。举个例子,假如 JVM 在业务高峰期有大量请求进来,此时堆内存使用量肯定会上升,假如此时堆内存的使用量为 5G,然后触发了 JVM 的垃圾回收,垃圾回收之后 JVM 堆内存使用量下降到了 1G,此时你用 top 命令去看这个 JVM 进程的 RES 列的值有很大可能还是 5G,原因就是这 5G 内存目前还归属 JVM 进程管理并使用,JVM 可能不会立即 把回收掉的堆空间还给操作系统。

只要 RES 列的值不大于我们给 JVM 设置 Xmx 的值,就没有关系。Xmx 这个参数的意思就是告诉操作系统,JVM 本大爷我有可能要使用 Xmx 这么大的内存,你操作系统提前做好心理准备。JVM 进程刚运行的时候并不会直接找操作系统要 Xmx 这么大的内存,JVM 在运行的过程中根据自己的情况一点一点问操作系统申请的。一旦 JVM 进程从操作系统申请到内存之后,JVM 在运行的过程中就可能就不会还给操作系统了。因为 JVM 跟操作系统之间如果总是借呀还呀的就会很浪费性能,没有什么必要。JVM 进程退出之后占用的这些内存肯定会释放给操作系统的。关于 JVM 是否把空闲的堆(heap)内存还给操作系统这件事,JVM 提供了一些参数:

-XX:MinHeapFreeRatio、

-XX:MaxHeapFreeRatio、

-XX:-ShrinkHeapInSteps

这几个参数你们自己查资料吧。

怎么验证这个说法?先找一个运行了 1 天以上的 JVM 进程,然后使用 top 命令看下这个 JVM 进程的 RES 列的值,然后再用我后面介绍的 jmap -heap 19463 命令看一下 JVM 进程堆的使用情况就知道了。

这块知识点摘自掘金 APP 上面 空无 大神的文章《 运维:你们 JAVA 服务内存占用太高,还只增不减!告警了,快来接锅 》。

熊峰 大神的文章《 JVM 调优之探索 CMS 和 G1 的物理内存归还机制 》。

stackoverflow 网站的提问《 Does GC release back memory to OS? 》。

Oracle 官方文档《 Default Option Values for Heap Size 》。

3 JAVA 自带命令–jstat,查看 GC(垃圾回收情况)

jstat 这个命令在 JDK 的安装目录 bin/下面


3.1 使用 jstat 查看 GC(垃圾回收) 的情况

jstat -gc jvm 进程 PID 2000

这个命令会每隔 2 秒统计一下 JVM 进程(PID):19463 的垃圾收集情况,命令最后 2000 的意思就是每隔 2 秒统计一次。使用 jstat 命令可以实时监测到 YGC 和 FGC 的情况,包括每次 YGC 和 FGC 各花费了多长时间,到目前为止总共进行了多少次 YGC 和 FGC。


jstat -gcutil jvm 进程 PID 2000

这个命令里面的-gcutil 监视内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比 。


也可以写成 jstat -gcutil jvm 进程 PID 2s , 2s 也是 2 秒钟刷新一次的意思。还有一种写法是 jstat -gcutil 19463 2s 5 ,这个命令是 2 秒钟刷新一次,总共统计 5 次就行了,5 次之后自动结束 jstat 命令,不需要你手工按 crtl+c 去终止命令。


–gcutil 监视内容 与 -gc 基本相同,但输出主要关注已使用空间占总空间的 百分比 。

jstat -gccause 28549 2000

gccause 与-gcutil 功能一样,但是会额外输出导致上一次 GC 产生的原因


工作中我个人使用 jstat -gccause 和 jstat -gc 这个俩个命令比较多。

3.2 jstat 输出内容解释

  • jstat -gc jvm 进程 PID 2000


jvm 的堆(heap)空间由 S0(Survivor,0 号幸存区)+S1(Survivor,1 号幸存区)+Eden(年轻代)+Tenured(Old 老年代)+Permanent(永久代)组成的。

注意:Permanent(永久代)在 jdk1.7 还是 jdk1.8 的时候被移除了,换成 Metaspace(元数据)了。注意,永久代的意思并不是这块内存永远不会回收,在发生 FullGC 的时候,永久代里面的垃圾也会被回收掉。

所以 jstat 的输出结果说明为:

S0C(Capacity):S0 的最大内存,总内存。Capacity 就是容量的意思。单位:kb

S0U(Used):S0 目前已经使用的大小。Used 就是已经使用的意思。单位:kb

EC,EU:就是年轻代

PC,PU:就是永久代

OC,OU:就是老年代

YGC:就是年轻代的 GC 次数

YGCT:就是年轻代 GC 所花费的时间,单位秒

FGC:就是 FGC 的次数

FGCT:就是 FGC 所花费的时间,单位秒

GCT:就是 YGC+FGC 俩个 GC 加起来所花费的时间,单位秒

  • jstat -gcutil 4777 2000 5


这个命令里面的-gcutil 监视内容与-gc 基本相同,但输出主要关注已使用空间占总空间的百分比,所以-gcutil 看到的是使用率。

这些命令你会使用了,关键结果你能看得懂吗?其实很简单,我们主要关注,年轻代和老年代和持久代的使用率,目前用了多少 G,最大的堆内存空间配置的是多少?是不是快满了,是不是快要内存溢出了就行。GC 前后的年轻代和老年代占用的空间是否减少了,如果发生了一次 GC,年轻代和老年代占用的空间并没有减少,那说明你的代码发生了内存泄漏。要赶紧使用我下面介绍的 jmap 命令将 java 的堆现场的情况 dump 下来使用 MAT 软件或者 GCeasy 或者 visualVM 或者国内 PerfMa 社区的软件来分析 dump 内存文件,找到代码泄漏的真正原因。Perfma 社区的 阿飞 Javaer 大神说 FullGC 一天超过一次肯定就不正常了,发现 FullGC 频繁的时候优先调查内存泄漏问题。我认为这个说法不太对,我看了一下,我们生产环境的 GC 情况,FullGC 一天 500 次左右,服务也挺正常的。并且老年代回收完使用率才 13%,说明我们生产环境 FullGC 是可以把垃圾回收掉的。FullGC 的次数本质是跟 JVM 的内存使用量有关系的,如果你们的系统业务很繁忙,FullGC 次数多也是正常的,只有 GC 之后能把垃圾都回收掉就可以。并且每次 FullGC 的 STW 线程停顿时间不长也没有关系的。

4. JAVA 自带命令–jinfo,查看 JVM 的配置信息

jinfo 这个命令在 JDK 的安装目录 bin/下面


jinfo -flags 12832

这个命令可以查看我们给 JVM 设置的配置项和参数(默认+人工配置)


这个命令没啥说的,很简单就是看我们给 JVM 设置的一些参数信息。

jinfo -flag MaxPermSize 1919,注意这个命令 flag 后面没有带 s,这个命令用来看我们有没有给 JVM 设置 MaxPermSize 这个参数。如果有,就将设置的值显示出来。


可以看到我,我给 JVM 设置的永久代 MaxPermSize 的最大空间为:-XX:MaxPermSize=1073741824(1 个 G)

jinfo -flag HeapDumpPath 111552

jinfo -flag MetaspaceSize 111552

jinfo -flag 这个命令不常用,最常用的就是 jinfo -flags 这个带 s 的直接看所有的 JVM 配置。

使用 jinfo 命令可以看到我们指定的-Xmx 堆的最大值。这里还有一个经验就是,最好将 Xms(jvm 堆 heap 的初始化大小) -Xmx(jvm 堆 heap 的最大值),这个俩个的值设置为一样的,避免每次垃圾回收完成后 JVM 重新分配内存,可以防止在每次 GC 后进行内存重新分配,这块知识来自 Perfma 社区的 阿飞 Javaer

5. JAVA 自带命令–jmap,查看 heap(堆)的内存使用情况

jmap 这个命令在 JDK 的安装目录 bin/下面


jmap -heap 19463 查看 java 堆(heap)使用情况

注意看我的截图,一定要放大了截图,然后仔细看


jmap -histo 19463 查看堆内存(histogram)中的对象数量及大小


jmap -histo 19463 | head -n100 只显示前 100 行


或者使用 jmap -histo:live 19463 | more 查看,使用 more 命令查看


TIPS:more 命令按空格可以翻页查看,按小写字母 q 可以退出 more 命令。

jmap -histo:live 19463 这个命令会先触发 JVM 执行 GC(垃圾回收),然后再统计信息。为什么要先触发 GC 呢?因为这个命令 live 只统计活着的对象。

jmap -dump:format=b,file=9739_jvm_heap.hprof 9739

先解释一下什么叫做 dump。dump 的意思是转存储,那什么叫做转存储呢?转存储就是将内存(运存)中的数据导出,然后保存(持久化)下来。我们都知道 APP(程序)在运行的时候也会产生一些临时数据或者 APP(程序)在运行的时候也需要临时存储一些数据。这些数据的存储都是临时性的,一旦 APP(程序)运行结束,这些数据就都消失了。所以当 JVM 运行的过程中出现问题的时候,注意是 JVM 在运行的时候出问题了,我们就需要把 JVM 运行时内存(heap 堆)的情况 dump(转存储),然后分析一下 JVM 的 heap(堆)上面目前是什么情况?


注意:执行该命令的时候会将整个 JVM 上面的所有线程都暂停,如果你的 java 堆比较大,比如有 10 个 G 左右,那暂停的时间可能比较长,有可能长达 10 分钟,所以在生产环境慎用这个命令。或者在生产环境,先让运维把请求都先负载到别的机器上面,再执行这个命令。

执行完成后在当前目录就会产生一个 9739_jvm_heap.hprof 的文件


jmap -dump: live ,format=b,file=heapLive.hprof 9739 如果带上 live 会先触发一次 GC,GC 完则只转存储活着的对象。

然后,这个文件是二进制的文件,人肯定是看不懂的。所以,需要借助分析 dump 文件的工具,可以使用工具来分析:

  • 国内 PerfMa 社区的 XElephant 在线工具网站为 https://console.perfma.com/ , https://memory.console.perfma.com/



  • GCeasy

网站为 https://gceasy.io/


  • Memory Analyzer (MAT)

网站为https://www.eclipse.org/mat/


MAT 这个工具有一个 MemoryAnalyzer.ini 配置文件:找到 MemoryAnalyzer.ini 文件,该文件里面有个 Xmx 参数,该参数表示最大内存占用量,默认为 1024m,根据堆转储文件大小修改该参数即可。MAT 工具要求 dump 文件的后缀名以.hprof 结尾。B 站 PerfMa up 主 JVM 字节码的探索与实践应用(第一期)直播回放 在第 56 秒说:如果 dump 文件太大,MAT 也会打不开。


至于这些分析 JAVA dump 文件的工具怎么用,你们可以自己去 Google 一下,我后面也会再写一篇关于这些工具的教程。不过有一点需要注意,如果你 dump 出来的文件很大,那么上面的工具就都不管用了,比如说你 dump 出来的文件大小为 10G,这么大的文件上面的工具想打开一个 10G 的文件也非常困难了。要知道分析 dump 文件的工具本身也是一个 APP,这个 APP 要分析这个 10G 的 dump 文件也需要把这个 10 个 G 的文件加载到自己的内存中去,10G 他肯定是不好加载的。这个时候你只能使用 jmap -histo 32924 | head -n 60 这个命令手动来查看堆内存上面什么对象最多了,这个命令的截图如下:


如果你发现这个命令的结果里面有你熟悉的类,那很大可能就是你项目中这个类的对象生成的太多了,并且使用完之后没有释放造成了内存泄漏,是有可能并不是绝对的,这个只能当线索去分析,不能当证据去使用。

如果你要看 jmap -histo 32924 这个命令的全部结果,可以使用 jmap -histo 32924 > /tmp/java_heap_32924.txt 这个命令,将命令的结果保存到文件中,然后将文件下载到本地查看。

这块知识来自 网络上的大神:Hollis Java 命令学习系列(三)——Jmap

博客园上面的大神:星朝 JVM 性能调优实践——JVM 篇

HeapDump 社区上面的大神 Coder 的技术之路 高并发服务优化篇:详解一次由读写锁引起的内存泄漏

HeapDump 社区上面的大神:Java 小能手 记一次线上服务 CPU 100%的处理过程

6. JAVA 自带命令–jstack,查看 JVM 内所有的线程运行情况

jstack 这个命令在 JDK 的安装目录 bin/下面


这个命令也比较简单,没啥好讲的, 但是非常重要,分析问题时超级有用。 命令就是 jstack -l JVMPID 就行了。这个命令配合我们前面讲的 top -Hp 19235 命令和 printf 将线程 ID 转换成 16 进制 printf “0x%x\n” 19235 ,能非常快速定位 JAVA 程序中运行卡顿和缓慢的性能问题。

jstack -l 9739


jstack -l 9739 > /tmp/9739_jvm_thread.dump

将 jstack -l 9739 的命令的全部输出结果都保存到文件里面去,然后再下载的本地或者网上去分析。


打开 JAVA 线程 dump 文件


打开 JAVA 线程 dump 文件之后,我们就可以利用上面我们讲过的 top -Hp 19235 命令和 printf 将线程 ID 转换成 16 进制 printf “0x%x\n” 19235 ,拿到最占 CPU 的线程 ID 之后,来这个 JAVA 线程 dump 文件里面搜索这个线程,就知道这个线程在干嘛了,卡在了哪一行。

grep ‘java.lang.Thread.State’ /tmp/9739_jvm_thread.dump | wc -l

统计总共有多少线程,线程总数。线程如果太多肯定是有问题的,至于多少线程算多我也不知道。观察呗,就跟袁隆平老爷子一样,你没事多下地观察一下水稻是怎么生长的就知道了,经验就是这么来的。你们项目平时正常运行的时候,你上去统计一下,等过一段时间你们项目真出问题了你跟平时的经验对比一下就小葱拌豆腐一清二白了。


grep “java.lang.Thread.State” /tmp/20210713_thread.log |sort| uniq -c | sort -nr

grep “java.lang.Thread.State”/tmp/20210713_vhlthread.log |sort| uniq -c | sort -nr 使用这个命令看下所有的线程都处于什么状态


上面介绍的命令使用起来毕竟太原始,效率太低了,不是人干的事。区分一个程序猿到底猴子还是人,主要看他会不会使用工具。介绍俩个分析线程快照的工具:

  • fastThread

网站为 https://fastthread 的具体使用教程,你们自己网上 Google 吧。.io/


分析结果




fastthread 的具体使用教程,你们自己网上 Google 吧。

  • 国内 HeapDump 社区的 XSheepdog 在线工具网站为 https://www.perfma.com/ , https://console.heapdump.cn/




更多用法需要你自己动手去挖掘。

如果您觉得文章对您有用,请转发、评论谢谢~

用户头像

还未添加个人签名 2021.10.14 加入

还未添加个人简介

评论

发布
暂无评论
JAVA应用生产问题排查步骤