内存泄漏的通用排查方法

内存泄漏的通用排查方法

本文聊一聊如何系统性地分析查找内存泄漏的具体方法,但不会具体到哪种语言和具体业务代码逻辑中,而是会从 Linux 系统上通用的一些分析方法来入手。这样,不论你使用什么开发语言,不论你在开发什么,它总能给你提供一些帮助。

如何定位谁在消耗内存

内存泄漏的外在表现通常是系统内存不够,严重的话可能会引起 OOM (Out of Memory),甚至系统宕机。那在发生这些现象时,惯用的分析套路是什么呢?

首先,我们需要去找出到底是谁在消耗内存。前文说过,/proc/meminfo 可以帮助我们来快速定位出问题所在。 但/proc/meminfo 中的项目很多,我们没必要全部都背下来,不过有些项是相对容易出问题的,也是你在遇到内存相关的问题时,需要重点去排查的。我将这些项列了一张表格,也给出了每一项有异常时的排查思路。

总之,如果进程的内存有问题,那使用 top 就可以观察出来;如果进程的内存没有问题, 那你可以从 /proc/meminfo 入手来一步步地去深入分析。

分析进程内存泄漏的原因

接下来,我们分析一个实际的案例,来看看如何分析进程内存泄漏是什么原因导致的。如果你已经使用 top 排查出了业务进程的内存异常,即业务进程的虚拟地址空间(VIRT)被消耗很大,但是物理内存 (RES)使用得却很少,所以他怀疑是进程的虚拟地址空间有内存泄漏。

我们在前面几篇文章说过,出现该现象时,可以用 top 命令观察(这是当时保存的生产环境信息,部分信息做了脱敏处理):

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

31108 app 20 0 285g 4.0g 19m S 60.6 12.7 10986:15 app_server

​ 可以看到 app_server 这个程序的虚拟地址空间(VIRT 这一项)很大,有 285GB。

那该如何追踪 app_server 究竟是哪里存在问题呢?我们可以用 pidstat 命令来追踪下该进程的内存行为,看看能够发现什么现象。

$ pidstat -r -p 31108 1

PID minflt/s majflt/s VSZ RSS %MEM Command

04:47:00 PM 31108 353.00 0.00 299029776 4182152 12.73 app_server

...

04:47:59 PM 31108 149.00 0.00 299029776 4181052 12.73 app_server

04:48:00 PM 31108 191.00 0.00 299040020 4181188 12.73 app_server

...

04:48:59 PM 31108 179.00 0.00 299040020 4181400 12.73 app_server

04:49:00 PM 31108 183.00 0.00 299050264 4181524 12.73 app_server

...

04:49:59 PM 31108 157.00 0.00 299050264 4181456 12.73 app_server

04:50:00 PM 31108 207.00 0.00 299060508 4181560 12.73 app_server

...

04:50:59 PM 31108 127.00 0.00 299060508 4180816 12.73 app_server

04:51:00 PM 31108 27.00 0.00 299070752 4180956 12.73 app_server

​ 如上所示,在每个整分钟的时候,VSZ 会增大 244KB,这看起来是一个很有规律的现象。然后,我们再来看下增大的这个内存区域到底是什么,你可以通过 /proc/PID/smaps 来看。

增大的内存区域,具体如下:

$ cat /proc/31108/smaps

...

7faae0e49000 - 7faae1849000 rw-p 00000000 00:00 0 #私有地址空间

Size: 10240 kB

Rss: 80 kB

Pss: 80 kB

Shared_Clean: 0 kB

Shared_Dirty: 0 kB

Private_Clean: 0 kB

Private_Dirty: 80 kB

Referenced: 60 kB

Anonymous: 80 kB

AnonHugePages: 0 kB

Swap: 0 kB

KernelPageSize: 4 kB

MMUPageSize: 4 kB

7faae1849000 - 7faae184a000 ---p 00000000 00:00 0 #保护页(进程无法访问)

Size: 4 kB

Rss: 0 kB

Pss: 0 kB

Shared_Clean: 0 kB

Shared_Dirty: 0 kB

Private_Clean: 0 kB

Private_Dirty: 0 kB

Referenced: 0 kB

Anonymous: 0 kB

AnonHugePages: 0 kB

Swap: 0 kB

KernelPageSize: 4 kB

MMUPageSize: 4 kB

​ 可以看到,它包括:一个私有地址空间,这从 rw -p 这个属性中的 private 可以看出来;以 及一个保护页 ,这从---p 这个属性可以看出来,即进程无法访问。对于有经验的开发者而 言,从这个 4K 的保护页就可以猜测出应该跟线程栈有关了。

然后我们跟踪下进程申请这部分地址空间的目的是什么!

通过 strace 命令来跟踪系统调用就可以了。因为 VIRT 的增加,它的系统调用函数无非是 mmap 或者 brk,那么我们只需 要 strace 的结果来看下 mmap 或 brk 就可以了。

用 strace 跟踪如下:

$ strace -t -f -p 31108 -o 31108.strace

线程数较多,如果使用 -f 来跟踪线程,跟踪的信息量也很大,逐个搜索日志里面的 mmap 或者 brk 真是眼花缭乱, 所以我们来 grep 一下这个大小 (10489856 即 10244KB),然后 过滤下就好了:

$ cat 31108.strace | grep 10489856

31152 23:00:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31157 23:02:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31158 23:03:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31165 23:04:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31163 23:05:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31153 23:06:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31155 23:07:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31149 23:08:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31147 23:09:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31159 23:10:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31157 23:11:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31148 23:12:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31150 23:13:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31173 23:14:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

从这个日志我们可以看到,出错的是 mmap() 这个系统调用,那我们再来看下 mmap 这个内存的目的:

31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

31151 23:01:00 mprotect(0x7fa94bbc0000, 4096, PROT_NONE <<<

31151 23:01:00 clone( <<< 创建线程

31151 23:01:00 <... clone resumed> child_stack=0x7fa94c5afe50, flags=CLONE_VM|

|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID

|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fa94c5c09d0, tls=0x7fa94c5c0700, child

可以看出,这是在 clone 时申请的线程栈。

到这里你可能会有一个疑问:既然线程栈消耗了这么多的内存,那理应有很多才对啊? 但是实际上,系统中并没有很多 app_server 的线程,那这是为什么呢?

答案其实比较简单:线程短暂执行完毕后就退出了,可是 mmap 的线程栈却没有被释放。

我们来写一个简单的程序复现这个现象,问题的复现是很重要的,如果很复杂的问题可以用简单的程序来复现,那就是最好的结果了。 如下是一个简单的复现程序:mmap 一个 40K 的线程栈,然后线程简单执行一下就退出。

#include

#include

#include

#include

#include

#include

#define _SCHED_H

#define __USE_GNU

#define STACK_SIZE 40960

int func(void *arg)

{

printf("thread enter.\n");

sleep(1);

printf("thread exit.\n");

return 0;

}

int main()

{

int thread_pid;

int status;

int w;

while (1) {

void *addr = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE);

if (addr == NULL) {

perror("mmap");

goto error;

}

printf("creat new thread...\n");

thread_pid = clone(&func, addr + STACK_SIZE, CLONE_SIGHAND|CLONE_FS);

printf("Done! Thread pid: %d\n", thread_pid);

if (thread_pid != -1) {

do {

w = waitpid(-1, NULL, __WCLONE | __WALL);

if (w == -1) {

perror("waitpid");

goto error;

}

} while (!WIFEXITED(status) && !WIFSIGNALED(status));

}

sleep(10);

}

error:

return 0;

}

然后我们用 pidstat 观察该进程的执行,可以发现它的现象跟生产环境中的问题是一致的:

$ pidstat -r -p 535 5

11:56:51 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command

11:56:56 PM 0 535 0.20 0.00 4364 360 0.00 a.out

11:57:01 PM 0 535 0.00 0.00 4364 360 0.00 a.out

11:57:06 PM 0 535 0.20 0.00 4404 360 0.00 a.out

11:57:11 PM 0 535 0.00 0.00 4404 360 0.00 a.out

11:57:16 PM 0 535 0.20 0.00 4444 360 0.00 a.out

11:57:21 PM 0 535 0.00 0.00 4444 360 0.00 a.out

11:57:26 PM 0 535 0.20 0.00 4484 360 0.00 a.out

11:57:31 PM 0 535 0.00 0.00 4484 360 0.00 a.out

11:57:36 PM 0 535 0.20 0.00 4524 360 0.00 a.out

^C

Average: 0 535 0.11 0.00 4435 360 0.00 a.out

你可以看到,VSZ 每 10s 增大 40K,但是增加的那个线程只存在了 1s 就消失了。 至此我们就可以推断出 app_server 的代码哪里有问题了,你只要去修复该代码 Bug,很快就会把该问题给解决。

当然了,应用程序的内存泄漏问题其实是千奇百怪的,分析方法也不尽相同,我们这个案例的目的是为了告诉你一些通用的分析技巧。掌握了这些通用分析技巧,很多时候就可以以不变来应万变了。

关注我,不迷路!更多嵌入式精品文章某信搜索共众 号【细说嵌入式】

结束语

本文讲述了系统性分析 Linux 上内存泄漏问题的分析方法,要点如下:

top 工具和 /proc/meminfo 文件是分析 Linux 上内存泄漏问题,甚至是所有内存问题 的第一步,我们先找出来哪个进程或者哪一项有异常,然后再针对性地分析;

百怪的,分析方法也不尽相同,我们这个案例的目的是为了告诉你一些通用的分析技巧。掌握了这些通用分析技巧,很多时候就可以以不变来应万变了。

结束语

本文讲述了系统性分析 Linux 上内存泄漏问题的分析方法,要点如下:

top 工具和 /proc/meminfo 文件是分析 Linux 上内存泄漏问题,甚至是所有内存问题 的第一步,我们先找出来哪个进程或者哪一项有异常,然后再针对性地分析;应用程序的内存泄漏千奇百怪,所以你需要掌握一些通用的分析技巧,掌握了这些技巧 很多时候就可以以不变应万变。但是,这些技巧的掌握,是建立在你的基础知识足够扎 实的基础上。你需要熟练掌握我们这个系列课程讲述的这些基础知识,熟才能生巧。

相关推荐

贷款中介企业老板必备:如何及时了解政策信息并利用助贷系统确保合规开展业务
简单气质的女生微信名字(精选550个)
世界杯365软件

简单气质的女生微信名字(精选550个)

📅 07-21 👁️ 1350
中超风云陆博飞哪里出 中超风云战术
365速发平台app下载

中超风云陆博飞哪里出 中超风云战术

📅 07-28 👁️ 8194