内存泄漏是指程序在运行过程中分配的内存无法被释放,导致内存使用量不断增加,最终可能导致程序崩溃或系统崩溃。
产生内存泄漏的原因
内存泄漏可能是由多种原因造成的,例如:
- 忘记释放内存。由于项目比较大,一般申请内存的地方与释放内存的地方并不在一起,导致容易遗漏。例如,在C语言中,如果使用malloc()函数分配了内存,就必须使用free()函数来释放它。否则,这块内存就会一直占用着,直到程序结束。
- 智能指针循环引用。多个对象相互引用,导致它们都无法被释放。同样在项目比较大的时候,由于维护的智能指针比较多,关系复杂时容易出现。例如,如果一个对象A引用了对象B,而对象B又引用了对象A,那么这两个对象都无法被释放。
- 全局变量:全局变量在程序的整个生命周期内都存在,即使它们不再被使用。在一定程度上来讲,也是一种泄漏。
如果是正常的使用全局变量,那内存的占用自然是合理的。但对于一些其它原因导致的内存泄漏,那就让人很头大了。影响小一点的,也就是自身应用的功能异常,影响大了,可能导致整个系统崩溃。
怎么搞
我写了一个模拟内存泄漏的小程序,来演示一下具体的排查过程,运行程序前,执行free是这样的:
root@gl:/home/gl# free
total used free shared buff/cache available
Mem: 2025004 109884 1792128 92 122992 1778336
Swap: 2097148 87868 2009280
执行后:
root@gl:/home/gl# free
total used free shared buff/cache available
Mem: 2025004 1181228 711804 92 131972 702484
Swap: 2097148 84796 2012352
由于模拟的小程序逻辑比较简单,就先不放出名字了。实际线上环境排查问题的时候,面临比较多的也是这样的一个场景:有比较多的应用跑在一台服务器上,开始时是一头雾水的。
由free的输出可以看出,系统总内存为2025004KB,即2GB,原有空闲内存1792128KB,约1.7GB,运行异常应用后,剩余内存一下子缩水到了711804KB,仅700MB。并且是used列增长了近1GB的大小,反而buf/cache变化不大。持续观察一会,会发现可用内存还在持续减少。这个时候的你:
这个时候,自然而然就能想到,需要找出是哪个进程占用内存比较多呢?
top:
命令行窗口输入top,输出默认是按cpu占用排序的,按 M 切换为按内存占用排序:
top - 03:00:12 up 57 min, 3 users, load average: 0.01, 0.01, 0.00
Tasks: 122 total, 2 running, 71 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.2 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 2025004 total, 710588 free, 1182252 used, 132164 buff/cache
KiB Swap: 2097148 total, 2012608 free, 84540 used. 701384 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1615 root 20 0 1055448 1.005g 1312 S 0.0 52.0 0:00.68 a.out
894 root 20 0 1411024 18612 5476 S 0.0 0.9 0:01.03 dockerd
713 root 20 0 1284616 12804 5932 S 0.0 0.6 0:06.93 containerd
403 root 19 -1 74340 10932 10596 S 0.0 0.5 0:00.24 systemd-journal
...
可疑进程"a.out"映入眼帘,虚拟内存VIRT与RES常驻内存占用高大1GB,跟free看到的内存增量也是能对应的上的。接下来要做的,就是看一下a.out这个程序是不是真的内存泄漏了。
内存泄漏检测工具
memleak是一个开源的内存泄漏检测工具,可以帮助我们检测C/C++程序中的内存泄漏。它可以跟踪程序的内存分配和释放情况,并报告任何可疑的内存泄漏。类似地,还有valgrind也可以用,但memleak的好处是不用重启进程,可以直接观察运行中的进程行为,所以这里用memleak。
执行memleak命令,并通过-p指定可疑进程pid:
root@gl:/home/gl# memleak-bpfcc -p 1615
Attaching to pid 1615, Ctrl+C to quit.
[03:06:12] Top 10 stacks with outstanding allocations:
10485760 bytes in 10 allocations from stack
fun+0x1f [a.out]
main+0x12 [a.out]
__libc_start_call_main+0x80 [libc.so.6]
[03:06:17] Top 10 stacks with outstanding allocations:
15728640 bytes in 15 allocations from stack
fun+0x1f [a.out]
main+0x12 [a.out]
__libc_start_call_main+0x80 [libc.so.6]
^Croot@gl:/home/gl#
观察一会后CTRL+C停止掉,从输出中可以看出,memleak在截至 [03:06:12] 的时间点,检测到了10次内存分配,总计分配了10485760字节(即10MB),这些分配尚未被释放。紧接着,可以看到申请内存的堆栈:即main函数中的fun调用。
接下来就比较简单了,找到a.out的源码分析一下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void fun() {
int i = 0;
while (i++ < 2048) {
int* p = (int*)malloc(1024 * 1024);
memset(p, 0, 1024 * 1024);
*p = i;
printf("%d MB\n", *p);
if (i > 1024) {
sleep(1);
}
}
}
int main() {
fun();
pause();
return 0;
}
看到这里你估计就要笑了,这是哪个沙雕写出的代码?很明显可以看出,是由于fun函数中的malloc没有调用对应的free,我们补上free调用就可以了。然而实际项目中,代码看起来并不是这么直接的,申请后的内存是要拿来使用的,你得找到一个合适的地方才能释放它。这就可以喝杯茶慢慢再分析了~