Linux下JVM常见问题处理

一、背景分析

线上故障主要会包括 CPU、内存磁盘以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。基本上出问题就是 df、free、top,然后依次 使用jstack、jmap,具体问题具体分析。

二、CPU分析

一般来讲我们首先会排查 CPU 方面的问题。原因包括业务逻辑问题(死循环)、频繁 GC 以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用 jstack 来分析对应的线程栈情况。

1. 使用 jstack 分析线程栈

先用 ps 命令找到对应进程的 pid有好几个目标进程,可以先 top 找到cpu较高的进程

 

 

 

接着top -H -p <pid> 找到 CPU 使用率比较高的一些线程

 

 

 

然后将占用最高的 pid 转换为 16 进制printf '%x\n' <pid>得到 nid

 

 

 

接着直接在 jstack 中找到相应的堆栈信息

jstack <pid> |grep '<nid>' -C5 –color

利用 jstack 生成虚拟机中所有线程的快照

jstack -l {pid} > {path}

cat {path} |grep '790D' -C 5

 

 

 

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

"http-nio-9001-exec-269" #536 daemon prio=5 os_prio=0 tid=0x00007f04700eb800 nid=0x790d runnable [0x00007f0435cb4000]

   java.lang.Thread.State: RUNNABLE

 

"http-nio-9001-exec-269"   线程名称

#536   线程编号

daemon   守护线程

prio=5   线程的优先级

os_prio=0   系统级别的线程优先级

tid=0x00007f04700eb800   线程id

nid=0x790d   native线程的id

runnable [0x00007f0435cb4000]   线程当前状态

对整个 jstack 文件进行分析,通常我们会比较关注 WAITING 和 TIMED_WAITING 的部分,如果 WAITING 之类的特别多,那么多半是有问题。

cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c

 

 

 

2. 频繁 GC

先确定下 gc 是不是太频繁,使用jstat -gc pid 1000命令来对 gc 分代变化情况进行观察,1000 表示采样间隔(ms)

jstat -gc pid 1000

 

 

 

 

 

 

如果看到 gc 比较频繁,再针对 gc 方面做进一步分析。

3. 上下文切换

针对频繁上下文问题,我们可以使用vmstat命令来进行查看cs(context switch)一列则代表了上下文切换的次数。

 

 

 

如果对特定的 pid 进行监控可以使用 pidstat -w pid命令cswch 和 nvcswch 表示自愿及非自愿切换。

pidstat -w <pid>

 

 

 

 

4. 分析过程

(1) 找到CPU使用率最高的进程

(2) 找到该进程中CPU使用率最高的线程

(3) 利用jstack生成该进程下所有线程快照

(4) 在快照中找到对应的线程分析问题

三、磁盘

首先是磁盘空间方面,使用df -hl来查看文件系统状态

df -hl

 

 

 

更多时候磁盘问题还是性能上的问题可以通过 iostat -d -k -x来进行分析

iostat -d -k -x

 

 

 

最后一列%util可以看到每块磁盘写入的程度,而rrqpm/s以及wrqm/s分别表示读写速度,一般就能定位到具体哪块磁盘出现问题了。

四、内存

主要包括 OOM、GC 问题和堆外内存。一般来先用free命令先来检查内存的各种情况。

free

或者

free -h

 

 

 

1. 堆内内存

内存问题大多还都是堆内内存问题。表象上主要分为 OOM 和 Stack Overflow

(1) OOM

JMV 中的内存不足,OOM 大致可以分为以下几种:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

没有足够的内存空间给线程分配 Java 栈,基本上还是线程池代码写的有问题,比如说忘记 shutdown,JVM 方面可以通过指定Xss来减少单个 thread stack 的大小。另外也可以在系统层面增大 os 对线程的限制

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

堆的内存占用已经达到-Xmx 设置的最大值。解决思路仍然是先应该在代码中找,怀疑存在内存泄漏,通过 jstack 和 jmap 去定位问题。如果一切都正常需要通过调整Xmx的值来扩大内存。

Caused by: java.lang.OutOfMemoryError: Meta space

元数据区的内存占用已经达到XX:MaxMetaspaceSize设置的最大值,解决思路仍然是先应该在代码中找,怀疑存在内存泄漏,通过 jstack 和 jmap 去定位问题参数方面可以通过XX:MaxPermSize来进行调整。

java.lang.StackOverflowError

栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法运行的时候,请求新建栈帧时,栈所剩空间小于战帧所需空间线程栈需要的内存大于 Xss 值)。常见于通过深度递归调用方法,不停的产生栈帧,一直把栈空间堆满,直到抛出异常

Exception in thread "main" java.lang.StackOverflowError

(2) 使用 JMAP 定位代码内存泄漏

关于 OOM 和 Stack Overflo 的代码排查方面,我们一般使用 JMAP来导出 dump 文件

jmap -dump:format=b,file=filename <pid>

MAT(Eclipse Memory Analysis Tools)

mat独立版下载地址http://www.eclipse.org/mat/downloads.php

 

 

 

注:安装完成后,为了更有效率的使用 MAT,可以配置一些环境参数。因为通常而言,分析一个堆转储文件需要消耗很多的堆空间,为了保证分析的效率和性能,建议分配给 MAT 尽可能多的内存资源。可以采用如下两种方式来分配内存更多的内存资源给 MAT。

第一修改启动参数 MemoryAnalyzer.exe-vmargs -Xmx4g

第二编辑文件 MemoryAnalyzer.ini,在里面添加类似信息 -vmargs– Xmx4g。

1. MemoryAnalyzer.ini中的参数一般默认为-vmargs– Xmx1024m,这就够用了。假如机器的内存不大,改大该参数的值,会导致MemoryAnalyzer启动时,报错:Failed to create the Java Virtual Machine。

2.当导出的dump文件的大小大于配置的1024m(说明1中,提到的配置:-vmargs– Xmx1024m),MAT输出分析报告的时候,会报错:An internal error occurred during: "Parsing heap dump from XXX”。适当调大说明1中的参数即可。

 

mat(Eclipse Memory Analysis Tools)导入 dump 文件进行分析

 

 

 

内存泄漏问题一般直接选 Leak Suspects 即可,mat 给出了内存泄漏的建议。线程相关的问题可以选择 thread overview 进行分析。

 

IBM 内存分析工具 ha457.jar

IBM HeapAnalyzer 下载地址https://www.ibm.com/support/pages/ibm-heapanalyzer

 

 

 

 

 

 

 

 

 

 

 

生产环境自动dump出oom信息的phrof文件

使用启动参数 -XX:+HeapDumpOnOutOfMemoryError

导出地址:-XX:+HeapDumpPath={path}

2. GC 问题和线程

GC 问题除了影响 CPU 也会影响内存,排查思路也是一致的。一般先使用 jstat 来查看分代变化情况,比如 youngGC 或者 fullGC 次数是不是太多;EU、OU 等指标增长是不是异常等。

线程的话太多而且不被及时 gc 也会引发 oom,大部分是unable to create new native thread。

3. 堆外内存

堆外内存溢出表现就是物理常驻内存增长快,报错的话视使用方式都不确定,如果由于使用 Netty 导致的,那错误日志里可能会出现OutOfDirectMemoryError错误,如果直接是 DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory。

堆外内存溢出往往是和 NIO 的使用相关,一般我们先通过 pmap 来查看下进程占用的内存情况pmap -x pid | sort -rn -k3 | head -30,意思是查看对应 pid 倒序前 30 大的内存段。这边可以再一段时间后再跑一次命令看看内存增长情况,或者和正常机器比较可疑的内存段在哪里。

 

 

 

关键还是要看错误日志栈,找到可疑的对象,搞清楚回收机制,然后去分析对应的对象。比如 DirectByteBuffer 分配内存的话,是需要 full GC 或者手动 system.gc 来进行回收的。那么其实我们可以跟踪一下 DirectByteBuffer 对象的内存情况,通过jmap -histo:live pid手动触发 fullGC 来看看堆外内存有没有被回收。如果被回收了,那么大概率是堆外内存本身分配的太小了,通过-XX:MaxDirectMemorySize进行调整。如果没有什么变化,那就要使用 jmap 去分析那些不能被 gc 的对象,以及和 DirectByteBuffer 之间的引用关系了。

五、GC 问题

堆内内存泄漏总是和 GC 异常相伴。不过 GC 问题不只是和内存问题相关,还有可能引起 CPU 负载、网络问题等,只是相对来说和内存联系紧密些。

在启动参数中加上-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps来开启 GC 日志。GC 日志能大致推断出 youngGC 与 fullGC 是否过于频繁或者耗时过长

1. youngGC 过频繁

youngGC 频繁一般是短周期小对象较多,先考虑是不是新生代设置的太小了,看能否通过调整-Xmn、-XX:SurvivorRatio 等参数设置来解决问题。如果参数正常,但是 young gc 频率还是太高,就需要使用 Jmap 和 MAT 对 dump 文件进行进一步排查了。

-Xmn2G   设置年轻代大小为2G

-XX:SurvivorRatio=8   定义了新生代中Eden区域和Survivor区域的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10

2. youngGC 耗时过长

耗时过长问题就要看 GC 日志里耗时耗在哪一块了。

3. 触发 fullGC

(1)直接调用 System.gc() 时(调用后并不会立即发生 fullGC,后面会在某个时间点发生)

(2)老年代的可用空间不足时

(3)方法区空间不足时,或 Metaspace Space 使用达到 MetaspaceSize 但未达到 MaxMetaspaceSize 阈值;大多情况下扩容都会触发

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存时。由 Eden 区、From Survior 区向 To Survior 区复制时,对象大小大于 To Survior 区可用内存,则把该对象转存到老年代

(5)执行 jmap -histo:live 或者 jmap -dump:live;

分析过程

(1)找到内存使用率最高的进程

(2)利用jmap生成该进程的堆转储快照

(3)利用转储快照分析工具分析问题

六、总结

1. JVM 常用命令

jps:列出正在运行的虚拟机进程

jstat:监视虚拟机各种运行状态信息,可以显示虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据

jinfo:实时查看和调整虚拟机各项参数

jmap:生成堆转储快照,也可以查询 finalize 执行队列、Java 堆和永久代的详细信息

jstack:生成虚拟机当前时刻的线程快照

jhat:虚拟机堆转储快照分析工具

jmap 搭配使用,分析 jmap 生成的堆转储快照,与 MAT 的作用类似

2. 排查步骤

1、先找到对应的进程: PID

2、生成线程快照 stack (或堆转储快照: hprof )

3、分析快照(或堆转储快照),定位问题

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

 

 

 

4. 常用 JVM 性能检测工具

Eclipse Memory Analyer、JProfile、JProbe Profiler、JVisualVM、JConsole、Plumbr

posted @ 2021-08-30 15:50  Mr.Aaron  阅读(602)  评论(0编辑  收藏  举报