<audio id="audio" title="25 | 内存持续上升，我该如何排查问题？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/30/fc/300f6947de14c0197145f2122c8ea7fc.mp3"></audio>

你好，我是刘超。

我想你肯定遇到过内存溢出，或是内存使用率过高的问题。碰到内存持续上升的情况，其实我们很难从业务日志中查看到具体的问题，那么面对多个进程以及大量业务线程，我们该如何精准地找到背后的原因呢？

## 常用的监控和诊断内存工具

工欲善其事，必先利其器。平时排查内存性能瓶颈时，我们往往需要用到一些Linux命令行或者JDK工具来辅助我们监测系统或者虚拟机内存的使用情况，下面我就来介绍几种好用且常用的工具。

### Linux命令行工具之top命令

top命令是我们在Linux下最常用的命令之一，它可以实时显示正在执行进程的CPU使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息，下半部分显示的是进程的使用率统计信息。

<img src="https://static001.geekbang.org/resource/image/36/49/3633095ed54d1ef22fc08310497d6b49.jpg" alt="">

除了简单的top之外，我们还可以通过top -Hp pid查看具体线程使用系统资源情况：

<img src="https://static001.geekbang.org/resource/image/1e/47/1e4429a9785ae4e6c0884655ee8b5747.jpg" alt="">

### Linux命令行工具之vmstat命令

vmstat是一款指定采样周期和次数的功能性监测工具，我们可以看到，它不仅可以统计内存的使用情况，还可以观测到CPU的使用率、swap的使用情况。但vmstat一般很少用来查看内存的使用情况，而是经常被用来观察进程的上下文切换。

<img src="https://static001.geekbang.org/resource/image/31/62/31a79622cdcadda4e9003b075378dc62.jpg" alt="">

- r：等待运行的进程数；
- b：处于非中断睡眠状态的进程数；
- swpd：虚拟内存使用情况；
- free：空闲的内存；
- buff：用来作为缓冲的内存数；
- si：从磁盘交换到内存的交换页数量；
- so：从内存交换到磁盘的交换页数量；
- bi：发送到块设备的块数；
- bo：从块设备接收到的块数；
- in：每秒中断数；
- cs：每秒上下文切换次数；
- us：用户CPU使用时间；
- sy：内核CPU系统使用时间；
- id：空闲时间；
- wa：等待I/O时间；
- st：运行虚拟机窃取的时间。

### Linux命令行工具之pidstat命令

pidstat是Sysstat中的一个组件，也是一款功能强大的性能监测工具，我们可以通过命令：yum install sysstat安装该监控组件。之前的top和vmstat两个命令都是监测进程的内存、CPU以及I/O使用情况，而pidstat命令则是深入到线程级别。

通过pidstat -help命令，我们可以查看到有以下几个常用的参数来监测线程的性能：

<img src="https://static001.geekbang.org/resource/image/90/46/90d26ef49ad94510062ac3f36727a346.jpg" alt="">

常用参数：

- -u：默认的参数，显示各个进程的cpu使用情况；
- -r：显示各个进程的内存使用情况；
- -d：显示各个进程的I/O使用情况；
- -w：显示每个进程的上下文切换情况；
- -p：指定进程号；
- -t：显示进程中线程的统计信息。

我们可以通过相关命令（例如ps或jps）查询到相关进程ID，再运行以下命令来监测该进程的内存使用情况：

<img src="https://static001.geekbang.org/resource/image/18/61/184df3ee5ab0a920f506b3daa6250a61.jpg" alt="">

其中pidstat的参数-p用于指定进程ID，-r表示监控内存的使用情况，1表示每秒的意思，3则表示采样次数。

其中显示的几个关键指标的含义是：

- Minflt/s：任务每秒发生的次要错误，不需要从磁盘中加载页；
- Majflt/s：任务每秒发生的主要错误，需要从磁盘中加载页；
- VSZ：虚拟地址大小，虚拟内存使用KB；
- RSS：常驻集合大小，非交换区内存使用KB。

如果我们需要继续查看该进程下的线程内存使用率，则在后面添加-t指令即可：

<img src="https://static001.geekbang.org/resource/image/3c/72/3c9072c659a91b5f83cbc1a112ddcc72.jpg" alt="">

我们知道，Java是基于JVM上运行的，大部分内存都是在JVM的用户内存中创建的，所以除了通过以上Linux命令来监控整个服务器内存的使用情况之外，我们更需要知道JVM中的内存使用情况。JDK中就自带了很多命令工具可以监测到JVM的内存分配以及使用情况。

### JDK工具之jstat命令

jstat可以监测Java应用程序的实时运行情况，包括堆内存信息以及垃圾回收信息。我们可以运行jstat -help查看一些关键参数信息：

<img src="https://static001.geekbang.org/resource/image/42/e8/42880a93eb63ae6854a7920e73a751e8.jpg" alt="">

再通过jstat -option查看jstat有哪些操作：

<img src="https://static001.geekbang.org/resource/image/7a/7d/7af697d9cfd6002a49063ab2464d5f7d.jpg" alt="">

- -class：显示ClassLoad的相关信息；
- -compiler：显示JIT编译的相关信息；
- -gc：显示和gc相关的堆信息；
- -gccapacity：显示各个代的容量以及使用情况；
- -gcmetacapacity：显示Metaspace的大小；
- -gcnew：显示新生代信息；
- -gcnewcapacity：显示新生代大小和使用情况；
- -gcold：显示老年代和永久代的信息；
- -gcoldcapacity ：显示老年代的大小；
- -gcutil：显示垃圾收集信息；
- -gccause：显示垃圾回收的相关信息（通-gcutil），同时显示最后一次或当前正在发生的垃圾回收的诱因；
- -printcompilation：输出JIT编译的方法信息。

它的功能比较多，在这里我例举一个常用功能，如何使用jstat查看堆内存的使用情况。我们可以用jstat -gc pid查看：

<img src="https://static001.geekbang.org/resource/image/e5/68/e59188982cf5b75243a8c333bfead068.jpg" alt="">

- S0C：年轻代中To Survivor的容量（单位KB）；
- S1C：年轻代中From Survivor的容量（单位KB）；
- S0U：年轻代中To Survivor目前已使用空间（单位KB）；
- S1U：年轻代中From Survivor目前已使用空间（单位KB）；
- EC：年轻代中Eden的容量（单位KB）；
- EU：年轻代中Eden目前已使用空间（单位KB）；
- OC：Old代的容量（单位KB）；
- OU：Old代目前已使用空间（单位KB）；
- MC：Metaspace的容量（单位KB）；
- MU：Metaspace目前已使用空间（单位KB）；
- YGC：从应用程序启动到采样时年轻代中gc次数；
- YGCT：从应用程序启动到采样时年轻代中gc所用时间(s)；
- FGC：从应用程序启动到采样时old代（全gc）gc次数；
- FGCT：从应用程序启动到采样时old代（全gc）gc所用时间(s)；
- GCT：从应用程序启动到采样时gc用的总时间(s)。

### JDK工具之jstack命令

这个工具在模块三的[答疑课堂](https://time.geekbang.org/column/article/105234)中介绍过，它是一种线程堆栈分析工具，最常用的功能就是使用 jstack pid 命令查看线程的堆栈信息，通常会结合top -Hp pid 或 pidstat -p pid -t一起查看具体线程的状态，也经常用来排查一些死锁的异常。

<img src="https://static001.geekbang.org/resource/image/28/88/2869503e8d5460e36b3fd3e1a52a8888.jpg" alt="">

每个线程堆栈的信息中，都可以查看到线程ID、线程的状态（wait、sleep、running 等状态）以及是否持有锁等。

### JDK工具之jmap命令

在[第23讲](https://time.geekbang.org/column/article/108139)中我们使用过jmap查看堆内存初始化配置信息以及堆内存的使用情况。那么除了这个功能，我们其实还可以使用jmap输出堆内存中的对象信息，包括产生了哪些对象，对象数量多少等。

我们可以用jmap来查看堆内存初始化配置信息以及堆内存的使用情况：

<img src="https://static001.geekbang.org/resource/image/80/3f/808870b42f5f6525d79f70fd287a293f.jpg" alt="">

我们可以使用jmap -histo[:live] pid查看堆内存中的对象数目、大小统计直方图，如果带上live则只统计活对象：

<img src="https://static001.geekbang.org/resource/image/74/7b/74f42fa2b48ceaff869472f6061c1c7b.jpg" alt="">

我们可以通过jmap命令把堆内存的使用情况dump到文件中：

<img src="https://static001.geekbang.org/resource/image/f3/17/f3c17fd9bb436599fb48cf151ee7ba17.jpg" alt="">

我们可以将文件下载下来，使用 [MAT](http://www.eclipse.org/mat/) 工具打开文件进行分析：

<img src="https://static001.geekbang.org/resource/image/3c/43/3cc14844625cebcc1cdb836e5ccbfc43.jpg" alt="">

下面我们用一个实战案例来综合使用下刚刚介绍的几种工具，具体操作一下如何分析一个内存泄漏问题。

## 实战演练

我们平时遇到的内存溢出问题一般分为两种，一种是由于大峰值下没有限流，瞬间创建大量对象而导致的内存溢出；另一种则是由于内存泄漏而导致的内存溢出。

使用限流，我们一般就可以解决第一种内存溢出问题，但其实很多时候，内存溢出往往是内存泄漏导致的，这种问题就是程序的BUG，我们需要及时找到问题代码。

**下面我模拟了一个内存泄漏导致的内存溢出案例，我们来实践一下。**

我们知道，ThreadLocal的作用是提供线程的私有变量，这种变量可以在一个线程的整个生命周期中传递，可以减少一个线程在多个函数或类中创建公共变量来传递信息，避免了复杂度。但在使用时，如果ThreadLocal使用不恰当，就可能导致内存泄漏。

这个案例的场景就是ThreadLocal，下面我们模拟对每个线程设置一个本地变量。运行以下代码，系统一会儿就发送了内存溢出异常：

```
    @RequestMapping(value = &quot;/test0&quot;)
    public String test0(HttpServletRequest request) {
        ThreadLocal&lt;Byte[]&gt; localVariable = new ThreadLocal&lt;Byte[]&gt;();
        localVariable.set(new Byte[4096*1024]);// 为线程添加变量
        return &quot;success&quot;;
    }

```

在启动应用程序之前，我们可以通过HeapDumpOnOutOfMemoryError和HeapDumpPath这两个参数开启堆内存异常日志，通过以下命令启动应用程序：

```
java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof  -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log heapTest-0.0.1-SNAPSHOT.jar

```

首先，请求test0链接10000次，这个时候我们请求test0的接口报异常了。

<img src="https://static001.geekbang.org/resource/image/60/dc/60ab8d7847a55a9bcf84d17ecd11ebdc.jpg" alt="">

通过日志，我们很好分辨这是一个内存溢出异常。我们首先通过Linux系统命令查看进程在整个系统中内存的使用率是多少，最简单就是top命令了。

<img src="https://static001.geekbang.org/resource/image/d2/37/d2ad570e1fff2a64a1924c2852f93e37.jpg" alt="">

从top命令查看进程的内存使用情况，可以发现在机器只有8G内存且只分配了4G内存给Java进程的情况下，Java进程内存使用率已经达到了55%，再通过top -Hp pid查看具体线程占用系统资源情况。

<img src="https://static001.geekbang.org/resource/image/6f/a7/6fdea40b5ff4f2f0744e019c3bef79a7.jpg" alt="">

再通过jstack pid查看具体线程的堆栈信息，可以发现该线程一直处于 TIMED_WAITING 状态，此时CPU使用率和负载并没有出现异常，我们可以排除死锁或I/O阻塞的异常问题了。

<img src="https://static001.geekbang.org/resource/image/4b/87/4bfb58d626f988260e016a2bdf0e8687.jpg" alt="">

我们再通过jmap查看堆内存的使用情况，可以发现，老年代的使用率几乎快占满了，而且内存一直得不到释放：

<img src="https://static001.geekbang.org/resource/image/fe/71/feb358259ea8b3ed2b67e868c101d271.jpg" alt="">

通过以上堆内存的情况，我们基本可以判断系统发生了内存泄漏。下面我们就需要找到具体是什么对象一直无法回收，什么原因导致了内存泄漏。

我们需要查看具体的堆内存对象，看看是哪个对象占用了堆内存，可以通过jmap查看存活对象的数量：

<img src="https://static001.geekbang.org/resource/image/c5/d9/c5b89deb306a2c470e606fa9c49dd0d9.jpg" alt="">

Byte对象占用内存明显异常，说明代码中Byte对象存在内存泄漏，我们在启动时，已经设置了dump文件，通过MAT打开dump的内存日志文件，我们可以发现MAT已经提示了byte内存异常：

<img src="https://static001.geekbang.org/resource/image/4c/63/4ceb91714afa77b54d1112a0e1f0c863.jpg" alt="">

再点击进入到Histogram页面，可以查看到对象数量排序，我们可以看到Byte[]数组排在了第一位，选中对象后右击选择with incomming reference功能，可以查看到具体哪个对象引用了这个对象。

<img src="https://static001.geekbang.org/resource/image/5a/91/5a651a2f52dfed72712543f7680de091.jpg" alt="">

在这里我们就可以很明显地查看到是ThreadLocal这块的代码出现了问题。

<img src="https://static001.geekbang.org/resource/image/2b/a2/2bed3871097249d64ccf4c79d68109a2.jpg" alt="">

## 总结

在一些比较简单的业务场景下，排查系统性能问题相对来说简单，且容易找到具体原因。但在一些复杂的业务场景下，或是一些开源框架下的源码问题，相对来说就很难排查了，有时候通过工具只能猜测到可能是某些地方出现了问题，而实际排查则要结合源码做具体分析。

可以说没有捷径，排查线上的性能问题本身就不是一件很简单的事情，除了将今天介绍的这些工具融会贯通，还需要我们不断地去累积经验，真正做到性能调优。

## 思考题

除了以上我讲到的那些排查内存性能瓶颈的工具之外，你知道要在代码中对JVM的内存进行监控，常用的方法是什么？

期待在留言区看到你的分享。也欢迎你点击“请朋友读”，把今天的内容分享给身边的朋友，邀请他一起讨论。


