Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

背景

内存泄漏一般指的是堆内存泄漏。在写代码的时候,通过C++运算符或者库函数(malloc等)分配内存,使用完后总是忘记释放内存,导致操作系统失去对这块内存的控制,也就是内存泄漏。

如果不进行内存泄漏的处理,当系统中运行的程序越来越多,内存泄漏也就会越来越多,即使实际上物理内存足够大,但是一旦内存泄漏越来越多,可用内存就越来越少,严重可能会导致系统崩溃。

如何定位可能发生内存泄漏的进程

模拟一个进程,该进程在运行时一直发生内存泄漏,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include <unistd.h>
#include <cstring>
using namespace std;

int main() {

while(true){
int* p = new int[10000];
memset(p, 0, 10000 * sizeof(int));
sleep(1);
}
return 0;
}

该代码一直在申请10000B字节,因为每次申请时都会覆盖掉申请内存的起始地址,所以该进程在运行时会一直发生内存泄漏。

发生内存泄漏的进程有一个特点,就是改进程在运行的过程中,其物理空间和虚拟空间会一直增加,可以通过Linux的 “ps aux| grep 监测进程名” 获得监测进程的pid,然后通过 watch 命令动态查看监测进程其内存的使用情况

1
2
3
4
ps aux|grep 进程名

每 0.5s 更新一次
watch -n 0.5 "ps -p <pid> -o pid,rss,vsz,%mem,cmd"

image-20250422185103711

image-20250422185251116

可以看出,随着时间的增长,进程13875的物理内存,虚拟内存,内存使用率一直增加,内存泄漏的进程一定是满足这个特征,但是满足这个特征的进程不一定是内存泄漏,可以使用watch命令观察一段时间。然后通过内存泄漏工具做具体的内存泄漏排查,常用的内存泄漏工具是Valgrind。

Valgrind工具安装

step1:下载安装包

1
wget https://sourceware.org/pub/valgrind/valgrind-3.16.1.tar.bz2

step2:解压安装包

1
tar -jxvf valgrind-3.16.1.tar.bz2

step3:配置Valgrind,生成Makefile文件

1
./configure

step4:编译 Valgrind

1
make

step5:安装 Valgrind

1
sudo make install

内存泄漏代码样例

1
2
3
4
5
6
7
#include<iostream>
using namespace std;

int main() {
int* p = new int[5];
return 0;
}

C++内存分布.drawio

这段代码可以看到 int* p = new int[5] ,指针变量p 和new 创建的一块内存的存储区域,指针 p存储在栈中, new 创建的空间存储在堆空间中,当这个函数执行完后,栈空间的内存会被回收,指针p会被自动销毁,但是new 创建的20个字节不会自动释放,这就造成了内存泄漏。

Valgrind 检查内存泄漏

查看全局进程内存泄漏大小

可以看出,泄漏了20个字节,和前面分析一致

1
2
g++ -g -o test oom.cpp
valgrind ./test

image-20250422172016971

定位具体行泄漏原因

1
2
g++ -g -o test oom.cpp
valgrind --leak-check=full ./test

image-20250422172542424

我们可以看到,导致内存泄漏的是在代码的第5行,因为 new int[5]导致的。

如何解决内存泄漏

  1. 可以手动释放分配的内存

    new 和 delete 搭配, new[] 和 delete[] 搭配。

    但是这种方法不建议在日常开发中使用,因为可能会因为疏漏造成内存泄漏。

    1
    2
    3
    4
    5
    6
    7
    8
    #include<iostream>
    using namespace std;

    int main() {
    int* p = new int[5];
    delete[] p;
    return 0;
    }

    image-20250422173634066

  2. 使用智能指针

    智能指针实际上就是普通指针的封装,在离开作用域时会对申请的资源进行销毁

    1
    2
    3
    4
    5
    6
    7
    8
    #include <iostream>
    #include <memory>
    using namespace std;

    int main() {
    shared_ptr<int[]> p(new int[5]); // 自动使用 delete[]
    return 0; // 自动释放,无内存泄漏
    }
  3. 使用RALL技术

    实际上智能指针 shared_ptr也属于RALL技术,资源的生命周期和对象的生命周期一致,在更复杂的开发过程中,可以将资源的申请和释放分装成一个类,在这个类当中写好构造和析构,当类创建对象时,资源也被创建,当对象被销毁时,资源也被释放,从而避免了内存泄漏

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #include <iostream>
    using namespace std;

    class IntArray {
    private:
    int* data; // 存储动态数组
    size_t size; // 数组大小

    public:
    // 构造函数:分配内存
    IntArray(size_t n) : size(n), data(new int[n]) {
    cout << "Allocated " << n << " integers." << endl;
    }

    // 析构函数:释放内存
    ~IntArray() {
    delete[] data;
    cout << "Freed memory." << endl;
    }

    // 访问元素(可添加边界检查)
    int& operator[](size_t index) {
    return data[index];
    }
    };

    int main() {
    IntArray arr(5); // 自动分配 5 个 int
    arr[0] = 10; // 像普通数组一样访问

    // 不需要手动 delete,析构时自动释放
    return 0;
    }

评论