「Linux」- 内存泄漏(学习笔记)

问题描述

对应用程序来说,动态内存的分配和回收,是既核心又复杂的一个逻辑功能模块。

管理内存的过程中,也很容易发生各种各样的“事故”:
1)没正确回收分配后的内存,导致内存泄漏;
2)访问的是已分配内存边界外的地址,导致程序异常退出;

内存泄漏是如何产生的

用户空间内存包括多个不同的内存段,通常堆内存内存映射段容易产生内存泄漏。内核空间我们不需要担心(当然可能存在内存泄漏),我们这里关注的重点是用户空间的应用程序产生的内存泄漏,已经如何处理。

只读段

包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。

数据段

包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。

栈内存(Stack)

在程序中定义局部变量,比如整数数组 int data[64],就定义了一个可以存储 64 个整数的内存段。由于这是一个局部变量,它会从内存空间的栈中分配内存。

栈内存由系统自动分配和管理,一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。

堆内存(Heap)

很多时候我们事先并不知道数据大小,所以要用标准库函数 malloc() 在程序中动态分配内存。这时候系统就会从内存空间的堆中分配内存。

堆内存由应用程序自己来分配和管理,如果应用程序没有正确释放堆内存,就会造成内存泄漏。
1)程序退出,系统自动释放内存
2)或者需要应用程序明确调用库函数 free() 来释放它们

内存映射段

包括动态链接库共享内存

其中共享内存由程序动态分配和管理。所以如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。

内存泄漏的危害

内存泄漏会带来系列问题:
1)应用程序自己不能访问:应用程序已经忘记自己申请的内存,导致内存被“占用但闲置”;
2)系统也不能把它们再次分配给其他应用:对应的物理内存被占用,导致无法再分配;
3)内存泄漏不断累积,甚至会耗尽系统内存;

虽然操作系统会通过 OOM – Out of Memory 会结束进程,但是在这之前依旧会带来其他问题:
1)其他需要内存的进程,可能无法分配新的内存;
2)内存不足,又会触发系统的缓存回收以及 SWAP 机制,从而导致 I/O 性能问题;

问题排查:检查内存泄漏(memleak)

# docker run --name=app-mem-leak -itd feisky/app:mem-leak
# docker logs app

// 如何定位内存泄漏问题
// 无法使用 top ps 等工具,但是可以使用 vmstat 查看内存增长情况

# vmstat
 0  0     15777696       297236        51480      1548608   47 1135   243  1417 6364 15045  26   6  67   0   0
 0  0     15777696       278084        51628      1543464   63    0   144   160 5876 15071  29   4  66   0   0
 1  0     15777696       273948        51644      1544536   28    0    28    16 5440 14589  25   5  70   0   0
 1  0     15777184       269524        51796      1545372   39    0   128    57 6214 16156  27   5  67   0   0
 1  0     15776928       389988        51820      1544004   41    0    88   115 13738 14626  32   5  62   0   0
 0  0     15776928       383908        51828      1545032   16    0    17   327 5702 15272  26   5  69   0   0
 3  0     15776928       429984        51848      1545208   51    0   132   359 6342 16245  32   5  62   0   0
procs -----------------------memory---------------------- ---swap-- -----io---- -system-- --------cpu--------
 r  b         swpd         free         buff        cache   si   so    bi    bo   in   cs  us  sy  id  wa  st
 5  0     15776928       427984        51876      1545644    4    0     5    44 6154 14798  28   6  66   0   0
 0  0     15776928       413108        51896      1545164   43    0    45   129 5919 14790  29   5  66   1   0
 1  0     15776672       397232        52056      1551576  135    0   839   259 6042 15907  26   6  68   0   0
 2  0     15776160       396176        52064      1549036   25    0    27    11 5515 14400  24   5  71   0   0
 0  0     15776160       381812        52088      1548952   27    0    31   183 5672 14403  29   5  66   0   0

// 但是效果也不是很明显,可使用内存反而越来越多了,而 buff 与 cache 变化不大

// 还有一个麻烦的方法,用 top 或 ps 来观察进程的内存使用情况,然后找出内存使用一直增长的进程,最后再通过 pmap 查看进程的内存分布

// 我们还可以使用 memleak 检测内存泄漏
// memleak 好像要比 valgrind 进行内存泄漏检测要方便很多

# memleak-bpfcc -a -p $(pidof app)
Attaching to pid 638, Ctrl+C to quit.
[11:47:50] Top 10 stacks with outstanding allocations:
        addr = 7f74780d75e0 size = 8192
        addr = 7f74780d55d0 size = 8192
        addr = 7f74780d95f0 size = 8192
        addr = 7f74780db600 size = 8192
        32768 bytes in 4 allocations from stack
                fibonacci+0x1f [app]
                child+0x4f [app]
                start_thread+0xdb [libpthread-2.27.so]
[11:47:55] Top 10 stacks with outstanding allocations:
        addr = 7f74780dd610 size = 8192
        addr = 7f74780d75e0 size = 8192
        addr = 7f74780d55d0 size = 8192
        addr = 7f74780d95f0 size = 8192
        addr = 7f74780db600 size = 8192
        addr = 7f74780e3640 size = 8192
        addr = 7f74780df620 size = 8192
        addr = 7f74780e1630 size = 8192
        addr = 7f74780e5650 size = 8192
        73728 bytes in 9 allocations from stack
                fibonacci+0x1f [app]
                child+0x4f [app]
                start_thread+0xdb [libpthread-2.27.so]

// 从调用堆栈中可以看出是 fibonacci() 函数分配的内存没释放
// 如果显示 [unknown] 则是因为 memleak 没有找到程序文件,因此无法找到符号表

// 至于修复,这里不再展开,需要查看源码以定位问题。

1)先要确认内存是否被缓存 / 缓冲区占用,排除缓存 / 缓冲区
2)继续用 pidstat 或者 top,定位占用内存最多的进程
3)通过 vmstat 或者 sar 发现内存在不断增长后,可以分析中是否存在内存泄漏的问题
4)使用内存分配分析工具 memleak ,检查是否存在内存泄漏

问题排查:CentOS 7.4, bind-sdb-9.9.4-74.el7_6.1.x86_64, valgrind

关于内存泄漏问题总结

实际应用程序就复杂多了。比如说:
1)malloc() 和 free() 通常并不是成对出现,在每个异常处理路径和成功路径上都需要释放内存;
2)在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放
3)更复杂的是,在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放

Java

如果是 java 应用程序,Java 看到的是JVM 的堆栈。其实,jmap这些Java原生的工具更好用

对java进程可以先通过 jstat -gc pid 1s 每隔1s 查看当前进程gc情况,如果存在内存泄露的话,那么该对象存活时间会很长当然会晋升到老年代,所以通过看老年代变化趋势,如果增大的话,我们再使用jmap -histo:live pid 查看进程中heap区对象的个数和占用的空间大小,找出数量大的对象然后找到对应的类查看代码,是否会存在内存泄露问题。

注意:可以将jmap -histo:live pid > data.txt 导入到一个文件中,然后通过sort 根据对象个数或占用空间进行排序

ThreadLocal使用不当会导致内存泄露
当给线程池中的线程设置local值 threadLoacl.set(obj) 后没有通过 threadLoacl.remove()就会导致内存泄露

根据ThreadLocal实现代码上看,每个线程中都会有个ThreadLocalMap 这个map中的key为ThreadLocal对象,value就是对应set的值,当离开了作用域threadLocal就不会再指向ThreadLocal对象,由于ThreadLocalMap中的key为WeakReference 当该对象只有它自己指向时就会导致key变成了null,如果当前线程是在线程池中是会一直存活的,也就是map中的value值会一直指向堆的对象,从而导致了内存泄露

参考文献

Memory leak – Wikipedia