15 | 内存泄露

134 阅读3分钟

普通进程通过内核提供的虚拟内存工作,虚拟内存需通过页表映射到物理内存。进程调用 malloc() 分配虚拟内存时,物理内存的分配仅在首次访问触发缺页异常时进行。Linux 使用 Cache 和 Buffer 缓存文件及磁盘数据,以优化 CPU 与磁盘间的性能差异。动态内存的分配与回收是应用程序中的核心逻辑,但也容易引发问题,如内存泄漏和越界访问导致程序异常等。

回顾内存分配和回收,进程的内存空间包括栈、堆、只读段、数据段和内存映射段等。

  • :局部变量从栈分配,内存由系统自动管理,作用域结束后自动回收,无内存泄漏风险。
  • :动态内存从堆分配,需要程序手动管理并释放,未释放可能导致内存泄漏。
  • 只读段:存储代码和常量,不会动态分配,无内存泄漏风险。
  • 数据段:包括全局和静态变量,大小固定,无内存泄漏风险。
  • 内存映射段:动态链接库和共享内存,其中共享内存需手动管理,可能导致内存泄漏。

1. 案例

环境准备:

  • 操作系统:Ubuntu 18.04
  • 机器配置:2 CPU,8GB 内存
  • 预先安装 sysstat、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.list.d/iovisor.list
sudo apt-get update
sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)

注意:bcc-tools 需要内核版本为 4.1 或者更高,如果你使用的是 CentOS7,或者其他内核版本比较旧的系统,那么你需要手动升级内核版本后再安装。

运行案例

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

执行案例程序,看到如下输出说明执行正常。

$ docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
6th => 8
7th => 13

另外一个终端执行

# 每隔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一直在下降。此时无法判断是否存在内存泄露,因为程序使用内存是正常行为,需要借助工具memleak。

# -a 表示显示每个内存分配请求的大小以及地址
# -p 指定案例应用的PID号
$ 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函数并没有释放内存

正常输出应如上。

修改后的程序:

# 清理原来的案例应用
$ 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:

当然,在生产实际环境中内存泄露的排查并没有如此简单。