「Linux」- 内存泄漏(学习笔记)
更新日期:2020年08月05日
@IGNORECHANGE
对应用程序来说,动态内存的分配和回收,是既核心又复杂的一个逻辑功能模块。
管理内存的过程中,也很容易发生各种各样的“事故”:
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值会一直指向堆的对象,从而导致了内存泄露
参考文献