JVM 常见线上问题排查 --- CPU 100%、内存高

1,827 阅读5分钟

一、 如何查看进程启动时间,持续时长

ps -eo pid,lstart,etime,cmd |grep -v grep |grep 进程名称

pid 进程号
lstart 启动时间
etime 持续时长
cmd 启动命令

二、CPU 100% 排查

ooz8wbmu7i.png

1. 确定出问题的进程。

top -c 显示运行中的进程列表信息,shift + p 使列表按 cpu 使用率排序显示
发现 PID 为 28555 的 Java 进程占用 CPU 高达 200%,出现故障。
通过 ps aux | grep PID 命令,可以进一步确定是 tomcat 进程出现了问题。

2. 定位到具体线程或者代码

显示线程列表找到了耗时最高的线程(tid):
找到了耗时最高的线程 28802,占用CPU时间快两个小时了!

ps -mp pid -o THREAD,tid,time

或者

使用命令: top -Hp {pid} ,同样 shift + p 可按 cpu 使用率对线程列表进行排序

3. 其次将需要的线程ID(tid)转换为 16 进制格式:

如 tid=28802 的 16 进制为:7082

printf "%x\n" tid

4. 最后打印线程的堆栈信息:

jstack pid | grep tid -A 30

如:jstack 28555 | grep 7082 -A 30

jstack:Java提供的命令。可以查看某个进程的当前线程栈运行情况。
-A 10 表示查找到所在行的后10行 根据这个命令的输出可以定位某个进程的所有线程的当前运行状态、运行代码,以及是否死锁等等。

线程快照格式都是统一的,下面以一个线程快照简单说明下:

"main" #1 prio=5 os_prio=0 tid=0x0000000002792800 nid=0x3e1c runnable [0x00000000025cf000] 

image.png

三、内存过高排查方法

1. 回顾

首先我们先回顾下Java进程的内存分配,方便我们下面排查思路的阐述。

以我们线上使用的JDK1.8版本为例。JVM内存分配网上有许多总结,我就不再进行二次创作。

JVM内存区域的划分为两块:堆区和非堆区。

  • 堆区:就是我们熟知的新生代老年代。
  • 非堆区:非堆区如图中所示,有元数据区和直接内存。 image.png 这里需要额外注意的是:永久代(JDK8 的原生区)存放 JVM 运行时使用的类,永久代的对象在full GC时进行垃圾收集。

image.png

2. JVM内存参数设置

  • 堆内存设置 堆内存(总的)由 -Xms 和 -Xmx 分别设置最小和最大堆内存

New Generation 由 -Xmn 设置,-XX:SurvivorRatio=m 设置 Eden和 两个Survivor区的大小比值;-XX:NewRatio=n 设置 New Generation 和 Old Generation 的大小比值。

每个线程的堆栈大小由 ·-Xss· 设置,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

  • 非堆内存设置 非堆内存由 -XX:PermSize=n 和 -XX:MaxPermSize=n 分别设置最小和最大非堆内存大小

排查思路

image.png

1. 找到内存占有率最高的进程号

使用命令: top -c 显示运行中的进程列表信息, shift + m 按内存使用率进行排序

2. 利用 jmap 生成堆转储快照

PS命令可以查到具体进程的CPU占用情况,但是不能查到一个进程下具体线程的内存占用情况。

ps -mp 9004 -o THREAD,tid,time,rss,size,%mem

只好寻求其他方法了,幸好Java提供了一个很好的内存监控工具:jmap 命令

jmap 命令命令有下面几种常用的用法:

jmap [pid]
jmap -histo:live [pid] > a.log
jmap -dump:live,format=b,file=xxx.xxx [pid]

用得最多是后面两个。其中:
jmap -histo:live [pid]
可以查看当前Java进程创建的活跃对象数目和占用内存大小。

jmap -dump:live,format=b,file=xxx.xxx [pid]
则可以将当前Java进程的内存占用情况导出来,方便用专门的内存分析工具(例如:MAT:Memory Analyzer Tool)来分析。这个命令对于分析是否有内存泄漏很有帮助。

3. 利用 MAT 分析 dump 文件

注意:
默认情况下,mat 最大内存是 1024m ,而我们的 dump 文件往往大于 1024m,所以我们需要调整,在 mat 的 home 目录下找到 MemoryAnalyzer.ini ,将 -Xmx1024m 修改成大于 dump 大小的空间, 我把它改成了 -Xmx6G

四、总结

1. JVM 常用命令

jps:列出正在运行的虚拟机进程
jinfo:实时查看和调整虚拟机各项参数
jstat:监视虚拟机各种运行状态信息,可以显示虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据
jmap:生成堆转储快照,也可以查询 finalize 执行队列、Java 堆和永久代的详细信息
jstack:生成虚拟机当前时刻的线程快照
jhat:虚拟机堆转储快照分析工具,与 jmap 搭配使用,分析 jmap 生成的堆转储快照,与 MAT 的作用类似

jinfo
首先,我们使用 jinfo pid 查看当前 jvm 的堆相关参数:

image.png 可见,最大堆容量为:4G。

jstat
接下来,我们使用命令 jstat -gcutil pid 1s 5 查看5秒内当前堆占用情况:
image.png
如上,新生代已经满了(占用97.33%),老年代也已经满了(占用100%),同时 FGC 高达 967 次!FGC 的数量太大了,正常来说 FGC 应该占整个 GC(YGC+FGC)的 1%-5% 才正常。

jmap
除了 jstat 命令外,我们也可以使用 jmap -heap pid 查看下当前JVM堆情况
image.png

2. 排查步骤

1) 先找到对应的进程: PID
2) 生成线程快照 stack (或堆转储快照: hprof )
3) 分析快照(或堆转储快照),定位问题

3. 内存泄露、内存溢出和 CPU 100% 关系

image.png

参考

JVM 常见线上问题 → CPU 100%、内存泄露 问题排查
内存过高排查方法 MAT:一次线上内存泄漏排查
一次完整的JVM堆外内存泄漏故障排查记录 Java内存泄漏分析系列之三:jstat命令的使用及VM Thread分析