查看原文
其他

内存泄漏咋办?带你 2 招制敌

IT服务圈儿 2023-02-06

The following article is from 涛歌依旧 Author 点击关注👉👉

来源丨经授权转自 涛歌依旧(ID:ai_taogeyijiu_2021)

作者丨涛哥


大家好,我是涛哥。最近,后端服务又遇到了烦人的内存泄漏问题,费了一些精力才找出具体的原因,上线后进行了验证,一切OK.今天,简要来聊聊内存泄漏的定位方法。为方便大家理解,我用最简单的 demo 来说明 - mtrace 和 valgrind.


一. 内存泄漏简介

内存泄漏,是一个谈虎色变的问题。比如,使用了malloc, 但没有使用free, 或者使用了new, 但没有使用delete, 都会造成内存泄漏。为什么说内存泄漏会谈虎色变呢?因为:

  • 内存泄漏危害性大

  • 内存泄漏潜伏时间长

  • 内存泄漏不容易定位

那么,如果内存泄漏了,我们该如何去定位呢?在C/C++相关的面试中,不问这个问题的面试官,是不合格的;不会回答这个问题的面试者,也是不合格的。当然,这种说法,似乎有一点绝对。

有的面试者回答,只有小心配对使用malloc/free和new/delete, 就能避免内存泄漏。显然,这种面试者缺乏基本的工程实践认知,缺乏对敌人的敬畏。

有的面试者回答,使用智能指针,就能避免内存泄漏。显然,这只是一种预防机制。在实际项目中,各种复杂因素导致的后果是内存已经泄漏,需要定位。

还有的面试者,更是想出了出人意料的答案,那就是检查代码!这又是对敌人缺乏了解啊。大型工程的代码动辄几十万行,谁敢走读代码来查内存泄漏呢?


二. mtrace简介

我们来看下mtrace的用途:

ubuntu@VM-0-15-ubuntu:~$ man mtraceMTRACE(1) Linux user manual MTRACE(1)
NAME mtrace - interpret the malloc trace log
SYNOPSIS mtrace [option]... [binary] mtracedata
DESCRIPTION mtrace is a Perl script used to interpret and provide human readable output of the trace log contained in the file mtracedata, whose contents were produced by mtrace(3). If binary is provided, the output of mtrace also contains the source file name with line number information for problem locations (assuming that binary was compiled with debugging information).
       For more information about the mtrace(3) function and mtrace script usage, see mtrace(3).


显然,mtrace命令是用来分析malloc函数的trace log. 

那么,这个trace log是怎么生成的呢? 且看上面的see mtrace(3). 有的朋友看到这里,不知道怎么敲命令了,以为是:

ubuntu@VM-0-15-ubuntu:~$ man mtrace(3)-bash: syntax error near unexpected token `('ubuntu@VM-0-15-ubuntu:~$


其实,正确的姿势如下:

ubuntu@VM-0-15-ubuntu:~$ man 3 mtraceMTRACE(3) Linux Programmer's Manual MTRACE(3)
NAME mtrace, muntrace - malloc tracing
SYNOPSIS #include <mcheck.h>
void mtrace(void);
void muntrace(void);
DESCRIPTION The mtrace() function installs hook functions for the memory-allocation functions (malloc(3), realloc(3) memalign(3), free(3)). These hook functions record tracing information about memory allocation and deal[m location. The tracing information can be used to discover memory leaks and attempts to free nonallocated memory in a program.
The muntrace() function disables the hook functions installed by mtrace(), so that tracing information is no longer recorded for the memory-allocation functions. If no hook functions were successfully installed by mtrace(), muntrace() does nothing.
When mtrace() is called, it checks the value of the environment variable MALLOC_TRACE, which should con[m tain the pathname of a file in which the tracing information is to be recorded. If the pathname is suc[m cessfully opened, it is truncated to zero length.
If MALLOC_TRACE is not set, or the pathname it specifies is invalid or not writable, then no hook func[m tions are installed, and mtrace() has no effect. In set-user-ID and set-group-ID programs, MALLOC_TRACE is ignored, and mtrace() has no effect.


显然,mtrace函数是用来记录malloc的trace log的。

所以,对于mtrace, 我们有如下的基本认知:

  • mtrace函数记录malloc的trace log

  • mtrace命令分析上述记录的trace log

这也就是用mtrace来定位内存泄漏的原理。那么,具体如何来查内存泄漏呢?不要着急,继续往下看。


三. mtrace定位内存泄漏

首先,我们来写一段有内存泄漏的程序:

#include <stdio.h>
int main(){    setenv("MALLOC_TRACE", "test.log", "1"); mtrace();
int *p = (int *)malloc(2 * sizeof(int));
return 0;}

我们来分析一下这段程序:

  • setenv是设置相关环境变量。

  • mtrace函数记录malloc的trace log.

  • malloc函数用于分配堆内存


我们来编译并运行一下(注意在编译时带-g参数):

ubuntu@VM-0-15-ubuntu:~$ gcc -g test.cubuntu@VM-0-15-ubuntu:~$ ubuntu@VM-0-15-ubuntu:~$ ubuntu@VM-0-15-ubuntu:~$ ./a.outubuntu@VM-0-15-ubuntu:~$ ls test.log test.logubuntu@VM-0-15-ubuntu:~$ cat test.log = Start@ ./a.out:[0x4005eb] + 0x1649570 0x8@ /lib/x86_64-linux-gnu/libc.so.6:(clearenv+0x5d)[0x7f1bc48f7e9d] - 0x1649010@ /lib/x86_64-linux-gnu/libc.so.6:(tdestroy+0x4cf)[0x7f1bc49c291f] - 0x16490e0@ /lib/x86_64-linux-gnu/libc.so.6:[0x7f1bc4a3223c] - 0x1649100


显然,编译运行后,生成了trace log, 即test.log文件。用cat命令查看,貌似也发现不了什么东西,这是因为,姿势错了。

我们不仅仅要用test.log, 还要结合二进制文件a.out呢,如下:

ubuntu@VM-0-15-ubuntu:~$ mtrace a.out test.log- 0x00000000018ab010 Free 3 was never alloc'd 0x7fb41725fe9d- 0x00000000018ab0e0 Free 4 was never alloc'd 0x7fb41732a91f- 0x00000000018ab100 Free 5 was never alloc'd 0x7fb41739a23c
Memory not freed:----------------- Address Size Caller0x00000000018ab570      0x8  at /home/ubuntu/test.c:8ubuntu@VM-0-15-ubuntu:~$

Oh, nice啊!终于查出是第8行,存在内存泄漏。接下来,我们打算修复代码,并再次验证。


四. 修复后再验证

修复内存泄漏后,代码为:

#include <stdio.h>
int main(){    setenv("MALLOC_TRACE", "test.log", "1"); mtrace();
int *p = (int *)malloc(2 * sizeof(int)); free(p);
return 0;}

编译运行,并查看是否有内存泄漏:

ubuntu@VM-0-15-ubuntu:~$ gcc -g test.cubuntu@VM-0-15-ubuntu:~$ ubuntu@VM-0-15-ubuntu:~$ ubuntu@VM-0-15-ubuntu:~$ ./a.outubuntu@VM-0-15-ubuntu:~$ mtrace a.out test.log- 0x00000000006ad010 Free 4 was never alloc'd 0x7faa9b044e9d- 0x00000000006ad0e0 Free 5 was never alloc'd 0x7faa9b10f91f- 0x00000000006ad100 Free 6 was never alloc'd 0x7faa9b17f23cNo memory leaks.ubuntu@VM-0-15-ubuntu:~$


看到No memory leaks后,心情就好了,没有内存泄漏了。接下来,我们看valgrind工具。


五. valgrind简介

有的朋友可能还不熟悉linux, 没关系,我们一起来,先来man valgrind一下:

ubuntu@VM-0-15-ubuntu:~$ man valgrindVALGRIND(1) Release 3.11.0 VALGRIND(1)
NAME valgrind - a suite of tools for debugging and profiling programs
SYNOPSIS valgrind [valgrind-options] [your-program] [your-program-options]
DESCRIPTION Valgrind is a flexible program for debugging and profiling Linux executables. It consists of a core, which provides a synthetic CPU in software, and a series of debugging and profiling tools. The architecture is modular, so that new tools can be created easily and without disturbing the existing structure.
Some of the options described below work with all Valgrind tools, and some only work with a few or one. The section MEMCHECK OPTIONS and those below it describe tool-specific options.
This manual page covers only basic usage and options. For more comprehensive information, please see the HTML documentation on your system: $INSTALL/share/doc/valgrind/html/index.html, or online: http://www.valgrind.org/docs/manual/index.html.
TOOL SELECTION OPTIONS The single most important option.
--tool=<toolname> [default: memcheck] Run the Valgrind tool called toolname, e.g. memcheck, cachegrind, callgrind, helgrind, drd, massif, lackey, none, exp-sgcheck, exp-bbv, exp-dhat, etc.


可以看到,valgrind是一款程序调试和分析的工具,其最重要的功能是内存检查,而我们今天要介绍的内存泄漏检查,便是其一。

valgrind除了检查内存泄漏外,还能检查内存越界、内存异常释放等诸多问题。这也是当年在腾讯面试中,面试官补充提到的问题。

具体来说,就是可以通过valgrind的toolname参数,来定制使用valgrind功能。下面,一起来看下toolname参数的部分选项。


  • memcheck: 检查内存问题,如泄漏、越界、异常释放

  • callgrind:    分析程序性能

  • cachegrind: 分析cache

  • helgrind:     分析多线程竞争

  • massif:        分析堆

接下来,我们介绍使用valgrind的toolname参数中的memcheck, 并用实际例子来看看如何定位内存泄漏。


六. valgrind定位内存泄漏

开发人员都应该知道,内存泄漏是一个很严重的问题,不容忽视。说起来有点悲催,我大学毕业时,居然还没听说过内存泄漏。

往事不堪回首,那就不回首了。言归正传,进入正题。我们来写一段有内存泄漏的程序,如下(只有malloc, 没有free)

#include <stdio.h>#include <stdlib.h>char* getMemory(){ char *p = (char *)malloc(30); return p;} int main(){ char *p = getMemory(); p = NULL; return 0;}


很明显,这段程序存在内存泄漏。有的读者总要说,一眼就看出来的问题,你用valgrind整那么复杂干啥?

很显然,这种同学缺乏实战经验,对敌人没有清醒的认识。几十万行的代码,你能用肉眼很明显看出内存泄漏?

也有同学要说,先申请堆内存,赋值给p指针, 然后又不使用p指针, 这不是无聊吗(一位读者之前提的问题)?

同学,实际项目中肯定要使用p啊!而在本文中,我只是用一个demo例子,来实战说明内存泄漏及其定位方法。


话不多说,继续介绍valgrind吧!先来编译一下(注意:编译时带上-g参数,便于定位出具体的代码行),然后用valgrind分析:

ubuntu@VM-0-15-ubuntu:~$ g++ -g test.cppubuntu@VM-0-15-ubuntu:~$ubuntu@VM-0-15-ubuntu:~$ubuntu@VM-0-15-ubuntu:~$ valgrind --tool=memcheck --leak-check=yes --show-reachable=yes ./a.out==31560== Memcheck, a memory error detector==31560== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.==31560== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info==31560== Command: ./a.out==31560== ==31560== ==31560== HEAP SUMMARY:==31560== in use at exit: 30 bytes in 1 blocks==31560== total heap usage: 1 allocs, 0 frees, 30 bytes allocated==31560== ==31560== 30 bytes in 1 blocks are definitely lost in loss record 1 of 1==31560== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)==31560== by 0x400537: getMemory() (test.cpp:5)==31560== by 0x40054E: main (test.cpp:11)==31560== ==31560== LEAK SUMMARY:==31560== definitely lost: 30 bytes in 1 blocks==31560== indirectly lost: 0 bytes in 0 blocks==31560== possibly lost: 0 bytes in 0 blocks==31560== still reachable: 0 bytes in 0 blocks==31560== suppressed: 0 bytes in 0 blocks==31560== ==31560== For counts of detected and suppressed errors, rerun with: -v==31560== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Oh, so nice啊,可以清晰地看到,内存definitely lost了,并且知道是getMemory函数中,且在第5行,于是乎,找到了内存泄漏的地方了。

接下来,我们准备修复这个内存泄漏问题,然后使用valgrind进行再次验证。


七. 修复后再验证

修复内存泄漏后,代码为:

#include <stdio.h>#include <stdlib.h>char* getMemory(){ char *p = (char *)malloc(30); return p;} int main(){ char *p = getMemory(); if(p != NULL) { free(p); p = NULL; } return 0;}

编译一下,并再次使用valgrind来验证:

ubuntu@VM-0-15-ubuntu:~$ g++ -g test.cppubuntu@VM-0-15-ubuntu:~$ubuntu@VM-0-15-ubuntu:~$ubuntu@VM-0-15-ubuntu:~$ valgrind --tool=memcheck --leak-check=yes --show-reachable=yes ./a.out==2839== Memcheck, a memory error detector==2839== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.==2839== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info==2839== Command: ./a.out==2839== ==2839== ==2839== HEAP SUMMARY:==2839== in use at exit: 0 bytes in 0 blocks==2839== total heap usage: 1 allocs, 1 frees, 30 bytes allocated==2839== ==2839== All heap blocks were freed -- no leaks are possible==2839== ==2839== For counts of detected and suppressed errors, rerun with: -v==2839== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)ubuntu@VM-0-15-ubuntu:~$


快来看啊,All heap blocks were freed -- no leaks are possible, 所有的堆内存都释放了,不存在任何内存泄漏。看到这句话后,总算是心安了。


下次如果再见到内存泄漏,大家就不用害怕了。无论是在实际工作中,还是在面试中,内存泄漏都是很基础很重要的问题。今天先这样,咱们明天见



1、仅用一个HTML标签,实现带动画的抖音Logo

2、ConcurrentHashMap面试灵魂拷问,你能扛多久

3、几行代码,竟然就能做个聊天室!

4、硬核 | Redis 布隆(Bloom Filter)过滤器原理与实战

5、只会 pull 和 push?试试这 5 条提高效率的 Git 命令

点分享

点点赞

点在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存