目录
一、何为内存泄露?
何为内存泄漏?又是怎么发生的呢?通过对Linux系统内存工作原理的了解,我们知道:
-
普通进程通过页表将内核提供的虚拟内存映射到物理内存。
-
当进程通过 malloc() 申请虚拟内存后,系统并不会立即为其分配物理内存,而是在首次访问时,才通过缺页异常陷入内核中分配内存。
-
协调 CPU 与磁盘间的性能差异,提升系统的 I/O 性能,Linux 还会使用 Cache 和 Buffer ,分别把文件和磁盘读写的数据缓存到内存中。
也正如我们所了解的,对应用程序来说,动态内存的分配和回收是既核心又复杂的一个逻辑功能模块。管理内存的过程中,也很容易发生各种各样的“事故”,比如:
-
没正确回收分配后的内存,导致了泄漏。
-
访问的是已分配内存边界外的地址,导致程序异常退出,等等。
如下百度百科解释:
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄露是指:用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。
二、怎么发生内存泄漏?
进程的内存空间时,用户空间内存包括多个不同的内存段,比如只读段、数据段、堆、栈以及文件映射段等。这些内存段正是应用程序使用内存的基本方式。举个例子:
(1)程序中定义了一个局部变量,比如一个整数数组 int data[64] ,就定义了一个可以存储 64 个整数的内存段。
由于这是一个局部变量,它会从内存空间的栈中分配内存。
栈内存由系统自动分配和管理。一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。
(2)假如不知道数据大小,用到标准库函数 malloc() ,在程序中动态分配内存。这时候,系统就会从内存空间的堆中分配内存。
堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。如果应用程序没有正确释放堆内存,就会造成内存泄漏。
问题:栈真的不会内存泄漏嘛?具体在什么场景下会有?
其他内存段是否也会导致内存泄漏呢?
-
只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
-
数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
-
内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。
内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。内存泄漏不断累积,甚至会耗尽系统内存。
在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:
回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;
回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;
杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。
虽然,系统最终可以通过 OOM (Out of Memory)机制杀死进程,但进程在 OOM 前,可能已经引发了一连串的反应,导致严重的性能问题。比如,
-
其他需要内存的进程,可能无法分配新的内存;
-
内存不足,又会触发系统的缓存回收以及 SWAP 机制
-
从而进一步导致 I/O 的性能问题等等。
三、发生后定位处理?
内存泄漏的危害这么大,那我们应该怎么检测这种问题呢?特别是,如果你已经发现了内存泄漏,该如何定位和处理呢。
(1)怎么检查内存情况,判断有没有泄漏发生呢?
vmstat 工具(man vmstat获取帮助)
top 虽然能观察系统和进程的内存占用情况;对于内存泄漏问题,我们更应该关注内存使用的变化趋势。
# 每隔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 基本保持不变。
未使用内存在逐渐减小,而 buffer 和 cache 基本不变,这说明,系统中使用的内存一直在升高。但这并不能说明有内存泄漏,因为应用程序运行中需要的内存也可能会增大。比如说,程序中如果用了一个动态增长的数组来缓存计算结果,占用内存自然会增长。
(2)那怎么确定是不是内存泄漏?有没有简单方法找出让内存增长的进程,并定位增长内存用在哪儿呢?
memleak工具
top 或 ps 来观察进程的内存使用情况,然后找出内存使用一直增长的进程。最后再通过 pmap 查看进程的内存分布。要判断内存的变化情况,还需要你写一个脚本,来处理 top 或者 ps 的输出。
可以使用一个专门用来检测内存泄漏的工具memleak。memleak是bcc 软件包中的一个工具,安装bcc-tools执行 /usr/share/bcc/tools/memleak 就可以运行它
memleak 可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出一个未释放内存和相应调用栈的汇总情况(默认 5 秒)
# -a 表示显示每个内存分配请求的大小以及地址# -p 指定案例应用的PID号
$ /usr/share/bcc/tools/memleak -a -p pid
四、总结
应用程序可以访问的用户内存空间,由只读段、数据段、堆、栈以及文件映射段等组成。其中,堆内存和文件映射段,需要应用程序来动态管理内存段,所以我们必须小心处理。不仅要会用标准库函数 malloc() 来动态分配内存,还要记得在用完内存后,调用库函数 free() 来释放它们。
malloc() 和 free() 通常并不是成对出现,需要在每个异常处理路径和成功路径上都释放内存 。在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。更复杂的是,在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。
所以,为了避免内存泄漏,最重要的一点就是养成良好的编程习惯,比如分配内存后,一定要先写好内存释放的代码,再去开发其他逻辑。还是那句话,有借有还,才能高效运转,再借不难。
当然,如果已经完成了开发任务,可以用 memleak 工具,检查应用程序的运行中,内存是否泄漏。如果发现了内存泄漏情况,再根据 memleak 输出的应用程序调用栈,定位内存的分配位置,从而释放不再访问的内存。