当进程通过 malloc() 申请虚拟内存后,系统并不会立即为其分配物理内存,而是在首次访问时,才通过缺页异常陷入内核中分配内存.对应用程序来说,动态内存的分配和回收,是既核心又复杂的一个逻辑功能模块。管理内存的过程中,也很容易发生各种各样的“事故”.
一、内存的分配和回收及存在的泄漏点
1.1 栈上的内存分配与回收
在程序中定义了一个局部变量,比如一个整数数组 int data[64] ,就定义了一个可以存储 64 个整数的内存段。由于这是一个局部变量,它会从内存空间的栈中分配内存。
栈内存由系统自动分配和管理。一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题
1.2 堆上的内存分配与回收
用到标准库函数 malloc()在程序中动态分配内存。系统就会从内存空间的堆中分配内存
什么时候会用到呢?
定义一个整数数组 int data[64],这个是事先知道数组不会超过65个元素,如果事先不知道数据大小,就需要动态开辟内存。这部分内存是从堆上分配的。
堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。如果应用程序没有正确释放堆内存,就会造成内存泄漏。
1.3 只读段上内存分配与回收
只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
1.4 数据段上内存分配与回收
数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
1.5 数据段上内存分配与回收
内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。
结合这张图理解。
二、内存不足带来的负面影响
内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。内存泄漏不断累积,甚至会耗尽系统内存。系统最终可以通过 OOM (Out of Memory)机制杀死进程,但进程在 OOM前,可能已经引发了一连串的反应,导致严重的性能问题。
其他需要内存的进程,可能无法分配新的内存;内存不足,又会触发系统的缓存回收以及 SWAP 机制,从而进一步导致 I/O 的性能问题等等
三、实例
用一个计算斐波那契数列的案例,来看看内存泄漏问题的定位和处理方法
斐波那契数列是一个这样的数列:0、1、1、2、3、5、8…,也就是除了前两个数是 0 和1, 其他数都由前面两数相加得到, 用数学公式来表示就是 F(n)=F(n-1)+F(n-2),(n>=2),F(0)=0, F(1)=1
安装docker 和bcc软件
# install sysstat docker sudo apt-get install -y sysstat docker.io # Install bcc sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD echo "deb https://repo.iovisor.org/apt/bionic bionic main" | sudo tee /etc/apt/sources.l sudo apt-get update sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
运行案例
$ docker run --name=app -itd feisky/app:mem-leak
运行结果
$ docker logs app 2th => 1 3th => 2 4th => 3 5th => 5 6th => 8 7th => 13
这些数值每隔 1 秒输出一次
vmstat结果
# 每隔 3 秒输出一组数据 $ vmstat 3 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 6601824 97620 1098784 0 0 0 0 62 322 0 0 100 0 0 0 0 0 6601700 97620 1098788 0 0 0 0 57 251 0 0 100 0 0 0 0 0 6601320 97620 1098788 0 0 0 3 52 306 0 0 100 0 0 0 0 0 6601452 97628 1098788 0 0 0 27 63 326 0 0 100 0 0 2 0 0 6601328 97628 1098788 0 0 0 44 52 299 0 0 100 0 0 0 0 0 6601080 97628 1098792 0 0 0 0 56 285 0 0 100 0 0
从输出中你可以看到,内存的 free 列在不停的变化,并且是下降趋势;而 buffer 和cache 基本保持不变。
这个案例内存泄漏的比较慢,如果有其他大量分配和回收内存的应用,那用 vmstat 就观察不明显了。
未使用内存在逐渐减小,而 buffer 和 cache 基本不变,这说明,系统中使用的内存一直在升高。但这并不能说明有内存泄漏,因为应用程序运行中需要的内存也可能会增大。比如说,程序中如果用了一个动态增长的数组来缓存计算结果,占用内存自然会增长。
检测内存泄漏的工具memleak
# -a 表示显示每个内存分配请求的大小以及地址 # -p 指定案例应用的 PID 号 $ /usr/share/bcc/tools/memleak -a -p $(pidof app) WARNING: Couldn't find .text section in /app WARNING: BCC can't handle sym look ups for /app addr = 7f8f704732b0 size = 8192 addr = 7f8f704772d0 size = 8192 addr = 7f8f704712a0 size = 8192 addr = 7f8f704752c0 size = 8192 32768 bytes in 4 allocations from stack [unknown] [app] [unknown] [app] start_thread+0xdb [libpthread-2.27.so]
所以调用栈不能正常输出,最后的调用栈部分只能看到 [unknown] 的标志
把 app 二进制文件从容器中复制出来,然后重新运行memleak 工具:
$ docker cp app:/app /app $ /usr/share/bcc/tools/memleak -p $(pidof app) -a Attaching to pid 12512, Ctrl+C to quit. [03:00:41] Top 10 stacks with outstanding allocations: addr = 7f8f70863220 size = 8192 addr = 7f8f70861210 size = 8192 addr = 7f8f7085b1e0 size = 8192 addr = 7f8f7085f200 size = 8192 addr = 7f8f7085d1f0 size = 8192 40960 bytes in 5 allocations from stack fibonacci+0x1f [app] child+0x4f [app] start_thread+0xdb [libpthread-2.27.so]
从内存分配的调用栈来看fibonacci() 函数分配的内存没释放。
查看代码发现child() 调用了 fibonacci() 函数,但并没有释放 fibonacci() 返回的内存。
$ docker exec app cat /app.c ... long long *fibonacci(long long *n0, long long *n1) { // 分配 1024 个长整数空间方便观测内存的变化情况 long long *v = (long long *) calloc(1024, sizeof(long long)); *v = *n0 + *n1; return v; } v oid *child(void *arg) { long long n0 = 0; long long n1 = 1; long long *v = NULL; for (int n = 2; n > 0; n++) { v = fibonacci(&n0, &n1); n0 = n1; n1 = *v; printf("%dth => %lld\n", n, *v); sleep(1); } } . ..
在 child() 中加一个释放函数
void *child(void *arg) { ... for (int n = 2; n > 0; n++) { v = fibonacci(&n0, &n1); n0 = n1; n1 = *v; printf("%dth => %lld\n", n, *v); free(v); // 释放内存 sleep(1); } }
修复后运行
# 清理原来的案例应用 $ docker rm -f app # 运行修复后的应用 $ docker run --name=app -itd feisky/app:mem-leak-fix # 重新执行 memleak 工具检查内存泄漏情况 $ /usr/share/bcc/tools/memleak -a -p $(pidof app) Attaching to pid 18808, Ctrl+C to quit. [10:23:18] Top 10 stacks with outstanding allocations: [10:23:23] Top 10 stacks with outstanding allocations: