说在前面:本文主要介绍关于内存泄漏的相关知识,包括什么是内存泄漏,以及如何使用hook函数去判断是否存在内存泄漏以及发现内存泄漏具体位置。
1.什么是内存泄漏?
内存泄漏是程序中的一种资源管理错误,它发生在计算机程序中动态分配的内存没有被及时释放或无法释放的情况下。动态分配的内存通常也就是说堆内存,所以我们常说的内存泄漏就是指的堆内存的泄漏,这种情况通常发生在使用如C或C++这类需要手动管理内存的编程语言中。内存泄漏可能导致多种问题,包括降低程序性能和系统稳定性。
详细来说:
-
动态内存分配:在编程中,特别是在C和C++等语言中,程序员可以动态分配内存来存储数据。这是通过如
malloc
、calloc
、realloc
和new
等函数来完成的。 -
内存未释放:当动态分配的内存在使用完毕后没有被正确释放(通过
free
或delete
),这部分内存就不能被再次使用,这就是内存泄漏。 -
长期影响:随着程序的运行,内存泄漏可能逐渐积累,如果程序每次只有几个字节的内存泄漏,这在短期内是很难发现的,最终导致可用内存越来越少。在极端情况下,这可能导致程序或整个系统因内存不足而变得缓慢或崩溃。
-
难以检测和定位:内存泄漏可能不会立即导致显著的问题,因此它们可能在代码中隐藏一段时间。它们通常需要专门的工具(如Valgrind、LeakSanitizer)来检测。
-
常见原因:
- 忘记释放内存。
- 程序逻辑错误导致某些部分的内存无法被释放。
- 异常或早期返回导致跳过释放内存的代码。
- 循环引用(特别是在使用某些类型的智能指针时)。
在自动垃圾回收的语言(如Java或Python)中,内存泄漏的风险较低,因为垃圾回收器会自动管理不再使用的内存。但是即使在这些环境中,不正确的资源管理(例如持有不再需要的对象引用)仍然可能导致类似内存泄漏的问题。
2. 如何判断是否存在内存泄漏?
方法一:很多小伙伴可能会说可以通过htop指令去查看内存使用情况,但是这种通过肉眼观察的方式可能是很耗时的。如果内存是在几个字节内变化的,通过这种方式去判断内存泄漏也是比较困难的,但是你也可以通过htop做一个初步的判断,那我们怎样更加准确的判断呢?
方法二:在大多数情况下,内存泄漏是由于malloc/new后并没有通过free/delete进行内存释放,从而导致内存泄漏。为了更加准确的判断是否发生内存泄漏,我们可以在程序中添加一个计数器,当使用malloc/new分配内存后,计数器就+1,使用free/delete释放内存后,计数器就-1。在程序退出前判断计数器是否为0,如果不为0,就存在内存泄漏。这种方法缺点在于它也只是初略的判断是否发生内存泄漏,而且并不能保证计数器为0时就一定没有内存泄漏,它的优点在于实现起来比较简单。
方法三:我们使用hook去判断是否发生内存泄漏,这里我们先提供代码,在代码中我们通过将自定义的 my_malloc
和 my_free
函数视为“钩子函数”。这些函数“钩入”了标准的内存分配和释放过程,允许在执行实际的 malloc
和 free
操作之前和之后执行额外的操作。程序实现了一个基本的内存分配跟踪系统,可以检测简单的内存泄漏情况。程序中使用链表来记录每次通过my_malloc
分配的内存。当内存通过 my_free
释放时,相应的记录会从链表中移除。check_for_leaks
函数用于检查链表中是否还有未删除的记录,这些记录表示存在内存泄漏。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义一个简单的链表结构来跟踪内存分配
typedef struct Allocation {
void* address;
size_t size;
struct Allocation* next;
} Allocation;
Allocation* allocations = NULL;
//函数来添加内存分配记录
void add_allocation(void* p, size_t size){
Allocation* newAlloc = (Allocation*)malloc(sizeof(Allocation));
newAlloc->address = p;
newAlloc->size = size;
newAlloc->next = allocations;
allocations = newAlloc;
}
// 函数来移除内存分配记录
void remove_allocation(void* p) {
Allocation **ptr = &allocations;
while (*ptr) {
Allocation* entry = *ptr;
if (entry->address == p) {
*ptr = entry->next;
free(entry);
return;
}
ptr = &entry->next;
}
}
// 自定义的 malloc 函数
void* my_malloc(size_t size) {
void* p = malloc(size);
add_allocation(p, size);
return p;
}
// 自定义的 free 函数
void my_free(void* p) {
remove_allocation(p);
free(p);
}
// 函数来检查和报告内存泄漏
void check_for_leaks() {
Allocation* current = allocations;
if (current == NULL) {
printf("No memory leaks detected.\n");
} else {
printf("Memory leaks detected:\n");
while (current) {
printf("Leaked memory at address %p, size %zu\n", current->address, current->size);
current = current->next;
}
}
}
int main() {
// 用自定义的 my_malloc 和 my_free 代替标准的 malloc 和 free
void* a = my_malloc(10);
void* b = my_malloc(20);
my_free(a);
// 注意:我们没有释放 'b',这将被报告为内存泄漏
// 检查内存泄漏
check_for_leaks();
return 0;
}
3. 如何判断代码在哪里发生了泄漏?
方法一:很多小伙伴可能会说可以通过仔细阅读代码的方式去判断哪里发生了资源的泄漏或者内存的泄漏,这种方式需要你具有比较深的代码功底,而且如果代码超过几万行,通过这种方式去检测也是不切实际的,那我们怎样去判断呢?
方法二: 我们可以通过对2中的hook方法进行进一步优化,通过宏定义检测具体在哪里发生了内存泄漏。代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义一个结构体来保持内存分配的信息
typedef struct Allocation {
void* address;
size_t size;
const char* file;
int line;
struct Allocation* next;
} Allocation;
Allocation* allocations = NULL;
// 添加内存分配记录
void add_allocation(void* p, size_t size, const char* file, int line) {
Allocation* newAlloc = (Allocation*)malloc(sizeof(Allocation));
newAlloc->address = p;
newAlloc->size = size;
newAlloc->file = file;
newAlloc->line = line;
newAlloc->next = allocations;
allocations = newAlloc;
}
// 移除内存分配记录
void remove_allocation(void* p) {
Allocation **ptr = &allocations;
while (*ptr) {
Allocation* entry = *ptr;
if (entry->address == p) {
*ptr = entry->next;
free(entry);
return;
}
ptr = &entry->next;
}
}
// 自定义的 malloc 和 free 函数
void* my_malloc(size_t size, const char* file, int line) {
void* p = malloc(size);
add_allocation(p, size, file, line);
return p;
}
void my_free(void* p) {
remove_allocation(p);
free(p);
}
// 定义宏以捕获分配位置
#define MY_MALLOC(size) my_malloc(size, __FILE__, __LINE__)
#define MY_FREE(p) my_free(p)
// 检查内存泄漏
void check_for_leaks() {
Allocation* current = allocations;
if (current == NULL) {
printf("No memory leaks detected.\n");
} else {
printf("Memory leaks detected:\n");
while (current) {
printf("Leaked memory at address %p, size %zu, allocated at %s:%d\n",
current->address, current->size, current->file, current->line);
current = current->next;
}
}
}
// 主函数
int main() {
// 使用 MY_MALLOC 和 MY_FREE
void* a = MY_MALLOC(10);
void* b = MY_MALLOC(20);
MY_FREE(a);
// 注意:我们没有释放 'b',这将被报告为内存泄漏
// 检查内存泄漏
check_for_leaks();
return 0;
}
在这个示例中,我们首先定义了一个记录结构 Allocation
来跟踪内存分配的地址、大小、文件名和行号。我们的 my_malloc
和 my_free
函数分别用于分配和释放内存,同时记录或移除内存分配的记录。最后,check_for_leaks
函数用于在程序结束时检查是否有未释放的内存,并报告这些内存分配的来源。请注意,这个程序是一个简化的示例,主要用于演示目的。在复杂的实际应用中,内存泄漏的检测和管理可能需要更全面和健壮的方法。
另外,对于new/delete也可以通过这种方式去检查是否发生内存泄漏,大家可以尝试下。
小知识
malloc
、calloc
、realloc的区别:
malloc
、calloc
和 realloc
是 C 语言标准库中用于动态内存分配的函数,它们各有不同的特点和用途:
-
malloc
(Memory Allocation):- 功能:分配指定大小的内存块。
- 参数:接受一个
size_t
类型的参数,指定要分配的字节数。 - 内存初始化:分配的内存块内容不会被初始化,其内容是未定义的(通常是随机的或垃圾值)。
- 示例:
int* ptr = (int*)malloc(sizeof(int) * 10);
分配了足够存储 10 个整数的内存。
-
calloc
(Contiguous Allocation):- 功能:分配指定数量和指定大小的元素的连续内存块。
- 参数:接受两个
size_t
类型的参数,第一个是元素的数量,第二个是每个元素的大小。 - 内存初始化:分配的内存块内容被初始化为零。
- 示例:
int* ptr = (int*)calloc(10, sizeof(int));
分配并初始化了一个足够存储 10 个整数的内存块,每个整数都被初始化为 0。
-
realloc
(Re-Allocation):- 功能:改变之前分配的内存块的大小(扩大或缩小)。
- 参数:第一个参数是一个指向之前已分配内存的指针,第二个参数是新的大小。
- 内存初始化:如果内存块增大,新增加的部分不会被初始化。如果内存块减小,超出的部分将被丢弃。
- 示例:
ptr = (int*)realloc(ptr, sizeof(int) * 20);
更改了之前分配内存块的大小,使其能够存储 20 个整数。
总的来说,malloc
用于简单的内存分配,calloc
除了分配内存外还会进行初始化,而 realloc
用于修改已分配内存的大小。正确地使用这些函数对于防止内存泄漏和其他内存相关错误非常重要。使用完这些动态分配的内存后,应该使用 free
函数来释放它们。