下文从 CPU占用过高、死锁、内存泄露 等场景优化JVM。
一、cpu 占用过高
cpu 占用过高要分情况讨论,是不是业务上在搞活动,突然有大批的流量进来,而且活动结束后 cpu 占用率就下降了,如果是这种情况其实可以不用太关心,因为请求越多,需要处理的线程数越多,这是正常的现象。
话说回来,如果你的服务器配置本身就差,cpu 也只有一个核心,这种情况,稍微多一点流量就真的能够把你的 cpu 资源耗尽,这时应该考虑先把配置提升吧。
第二种情况,cpu 占用率长期过高,这种情况下可能是你的程序有那种循环次数超级多的代码,甚至是出现死循环了。排查步骤如下:
(1)用 top 命令查看 cpu 占用情况
这样就可以定位出 cpu 过高的进程。在 linux 下,top 命令获得的进程号和 jps 工具获得的 vmid 是相同的:
(2)用 top -Hp 命令查看线程的情况
可以看到是线程 id 为 7287 这个线程一直在占用 cpu
(3)把线程号转换为 16 进制
1 | [root@localhost ~]# printf "%x" 7287 |
记下这个 16 进制的数字,下面我们要用
(4)用 jstack 工具查看线程栈情况
1 | [root@localhost ~]# jstack 7268 | grep 1c77 -A 10 |
通过 jstack 工具输出现在的线程栈,再通过 grep 命令结合上一步拿到的线程 16 进制的 id 定位到这个线程的运行情况,其中 jstack 后面的 7268 是第(1)步定位到的进程号,grep 后面的是(2)、(3)步定位到的线程号。
从输出结果可以看到这个线程处于运行状态,在执行 com.spareyaya.jvm.service.EndlessLoopService.service
这个方法,代码行号是 19 行,这样就可以去到代码的 19 行,找到其所在的代码块,看看是不是处于循环中,这样就定位到了问题。
二、死锁
死锁并没有第一种场景那么明显,web 应用肯定是多线程的程序,它服务于多个请求,程序发生死锁后,死锁的线程处于等待状态(WAITING
或 TIMED_WAITING
),等待状态的线程不占用 cpu,消耗的内存也很有限,而表现上可能是请求没法进行,最后超时了。在死锁情况不多的时候,这种情况不容易被发现。
可以使用 jstack 工具来查看
(1)jps 查看 java 进程
1 | [root@localhost ~]# jps -l |
(2)jstack 查看死锁问题
由于 web 应用往往会有很多工作线程,特别是在高并发的情况下线程数更多,于是这个命令的输出内容会十分多。jstack 最大的好处就是会把产生死锁的信息(包含是什么线程产生的)输出到最后,所以我们只需要看最后的内容就行了
1 | Java stack information for the threads listed above: |
发现了一个死锁,原因也一目了然。
三、内存泄漏
我们都知道,java 和 c++ 的最大区别是前者会自动收回不再使用的内存,后者需要程序员手动释放。在 c++ 中,如果我们忘记释放内存就会发生内存泄漏。但是,不要以为 jvm 帮我们回收了内存就不会出现内存泄漏。
程序发生内存泄漏后,进程的可用内存会慢慢变少,最后的结果就是抛出 OOM 错误。发生 OOM 错误后可能会想到是内存不够大,于是把 - Xmx 参数调大,然后重启应用。这么做的结果就是,过了一段时间后,OOM 依然会出现。最后无法再调大最大堆内存了,结果就是只能每隔一段时间重启一下应用。
内存泄漏的另一个可能的表现是请求的响应时间变长了。这是因为频繁发生的 GC 会暂停其它所有线程(Stop The World
)造成的。
为了模拟这个场景,使用了以下的程序
1 | import java.util.concurrent.ExecutorService; |
运行参数是 -Xms20m -Xmx20m -XX:+PrintGC
,把可用内存调小一点,并且在发生 gc 时输出信息,运行结果如下
1 | ... |
可以看到虽然一直在 gc,占用的内存却越来越多,说明程序有的对象无法被回收。但是上面的程序对象都是定义在方法内的,属于局部变量,局部变量在方法运行结果后,所引用的对象在 gc 时应该被回收啊,但是这里明显没有。
为了找出到底是哪些对象没能被回收,我们加上运行参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.bin
,意思是发生 OOM 时把堆内存信息 dump 出来。运行程序直至异常,于是得到 heap.dump
文件,然后我们借助 eclipse 的 MAT 插件来分析,如果没有安装需要先安装。
然后 File->Open Heap Dump...
,然后选择刚才 dump
出来的文件,选择 Leak Suspects
MAT 会列出所有可能发生内存泄漏的对象
可以看到居然有 21260
个 Thread 对象,3386 个 ThreadPoolExecutor
对象,如果你去看一下 java.util.concurrent.ThreadPoolExecutor
的源码,可以发现线程池为了复用线程,会不断地等待新的任务,线程也不会回收,需要调用其 shutdown
方法才能让线程池执行完任务后停止。
其实线程池定义成局部变量,好的做法是设置成单例。
上面只是其中一种处理方法
在线上的应用,内存往往会设置得很大,这样发生 OOM 再把内存快照 dump 出来的文件就会很大,可能大到在本地的电脑中已经无法分析了(因为内存不足够打开这个 dump 文件)。这里介绍另一种处理办法:
(1)用 jps 定位到进程号
1 | C:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jps -l |
因为已经知道了是哪个应用发生了 OOM,这样可以直接用 jps 找到进程号 135988
(2)用 jstat 分析 gc 活动情况
jstat 是一个统计 java 进程内存使用情况和 gc 活动的工具,参数可以有很多,可以通过 jstat -help
查看所有参数以及含义
1 | C:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jstat -gcutil -t -h8 24836 1000 |
上面是命令意思是输出 gc 的情况,输出时间,每 8 行输出一个行头信息,统计的进程号是 24836
,每 1000 毫秒输出一次信息。
输出信息是 Timestamp
是距离 jvm 启动的时间,S0、S1、E 是新生代的两个 Survivor
和 Eden
,O 是老年代区,M 是 Metaspace
,CCS 使用压缩比例,YGC 和 YGCT 分别是新生代 gc 的次数和时间,FGC
和 FGCT
分别是老年代 gc 的次数和时间,GCT 是 gc 的总时间。虽然发生了 gc,但是老年代内存占用率根本没下降,说明有的对象没法被回收(当然也不排除这些对象真的是有用)。
(3)用 jmap 工具 dump 出内存快照
jmap 可以把指定 java 进程的内存快照 dump 出来,效果和第一种处理办法一样,不同的是它不用等 OOM 就可以做到,而且 dump 出来的快照也会小很多。
1 | jmap -dump:live,format=b,file=heap.bin 24836 |
这时会得到 heap.bin
的内存快照文件,然后就可以用 eclipse 来分析了。