Linux下delete后内存仍然无法释放

记录下工作中所遇到的问题
起因:公司自主研发的录屏文件系统管理服务器(主要用作录屏文件存储以及实时播放,当然也有普通文件上传下载的功能)。当我们客户公司对服务器进行测压时,发现频繁调用某个函数后内存会缓慢上升;而当调用结束后,内存只是下降了一小段(20%内存占用率可能只是降了2%左右),接下来等待一段时间后内存可能会降到低值也可能变化不明显。

根据多番测试,发现基本都和上述差不多,起初认定时程序中有内存泄漏,花了差不多一天的时间使用gperftools工具(一款很不错的性能测试工具,google的,和golang那套测试工具类似,同一个生态圈,哈哈)各种测试,依旧没找到程序的泄漏点,不得已只能根据内存开辟的高发点逐步调试测试。

调试测试结果:根据调试测试发现,在每次调用该测试函数时都会根据保存的图片帧大小开辟若干个小内存,最终这些小内存在最后会被释放掉,也就是说不可能发生内存泄漏(纠结了很久,还以为是频繁开辟小内存所致,又捣鼓了一顿优化内存开辟的,发现优化内存开辟后系统内存占有量又所下降,因此我觉得可能是系统函数调用的锅),最终找上了delete。下面写了一些分析过程,不感兴趣直接看结论吧。

以下内容借鉴了一些博主的内容,可雷同,也是我自己的分析过程及在网上查阅解决问题的过程

c++的内存管理机制

程序运行内存的两个概念:虚拟内存和物理内存
什么是物理内存?即你机器的真实内存,所谓的加装内存条实际上就是提升机器的物理内存。
什么是虚拟内存?即进程运行的内存,比如说在32位的机器上,你进程的运行内存是4G(计算机寻址能力2的32次方),而且由于存在虚拟内存机制,每个进程都有自己独立的进程空间。
!至于为什么8G的物理内存可以运行如此多的程序,感兴趣的朋友可以去搜操作系统内存分配机制学习哈!

从进程的角度来看,每个进程都认为自己独立的占有了4G的进程空间(这里不给图了,脑补下):
1G的内核态地址空间
3G的用户地址空间:
栈区
堆区
全局静态区(.bbs、.data)
代码区(.text)

栈的生长方向是由上往下压,栈的大小是受限制(RLIMIT_STACK)的,默认是8M;
栈和堆之间还有一个文件映射区(MMAP,文件映射内存,如动态库等);
堆的生长方向是由下往上顶;

需要注意的一点是:上面所说的都是虚拟内存。只有在真正使用到这片内存空间时,才会涉及到物理内存页的的分配(内核调度,缺页中断等)。Linux下动态内存分配、管理都是基于malloc和free的(c/c++),动态内存即虚拟空间堆区。malloc和free操作的也是虚拟地址空间。

malloc,动态内存分配函数。是通过brk(sbrk)和mmap这两个系统调用实现的。
<以下引用了大佬的话,简单易懂>
brk(sbrk)是将数据段(.data)的最高地址指针_edata往高地址推。mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种实现方式的区别大致如下:

brk(sbrk),性能损耗少, brk(sbrk)可能存在内存碎片(由于new和delete的顺序不同,可能存在空洞,又称为碎片);

mmap相对而言,性能损耗大,mmap不存在内存碎片(是物理页对齐的,整页映射和释放);

无论是通过brk(sbrk)还是mmap调用分配的内存都是虚拟空间的内存,只有在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

delete(内部使用free),动态内存释放函数。如果是brk(sbrk)分配的内存,直接调用brk(sbrk)并传入负数,即可缩小Heap区的大小;如果是mmap分配的内存,调用munmap归还内存。无论这两种那种处理方式,都会立即缩减进程虚拟地址空间,并归还未使用的物理内存给操作系统。

brk(sbrk)和mmap都是系统调用,如果程序中频繁的进行内存的扩张和收缩,每次都直接调用,当然可以实现内存精确管理的目的,但是随之而来的性能损耗也很显著。目前大多数运行库(glibc)等都对内存管理做了一层封装,避免每次直接调用系统调用影响性能。如此,就涉及到运行库的内存分配的算法问题了。

运行库内存分配算法:
glibc中的内存分配算法是基于dlmalloc实现的ptmalloc;关于dlmalloc的详细可以参考A Memory Allocator(或是学习分配内存块管理fastbin),下面主要讲下内存归还策略:
为了避免频繁的系统调用,dlmalloc在内部维护了一个内存池,方便内存重用。在内存池的处理逻辑上,所有调用delete释放的内存,并不是立即调用brk(sbrk)归还给操作系统,而是将这块内存挂在free-list(关于free_list可以根据名字大致理解下,有兴趣可以查阅上述资料)下面,后续可以进行可选的内存归并操作,将相邻的小内存合并为大内存,并检查是否达到malloc_trim的threshhold,如果达到了,则调用malloc_trim归还部分可用内存给操作系统。
glibc中设置了默认的threshhold值为128K,也就是说当内存<=128K时,即使程序调用了delete释放了这部分内存,这些内存也不会归还给操作系统。表现为:调用了delete之后,进程占用内存并没有减少。

列出glibc中部分设置:
DEFAULT_MXFAST 64 (for 32bit), 128 (for 64bit) // free-list(fastbin)最大内存块
DEFAULT_TRIM_THRESHOLD 128 * 1024 // malloc_trim的门槛值 128k
DEFAULT_TOP_PAD 0
DEFAULT_MMAP_THRESHOLD 128 * 1024 // 使用mmap分配内存的门槛值 128k
DEFAULT_MMAP_MAX 65536 // mmap的最大数量
这些参数都可以通过mallopt进行调整。
malloc_trim(0)可以立即执行trim操作,将内存还给操作系统。

测试:
循环new分配64K x 2048的内存空间,写入脏数据后,循环调用delete释放。top看进程依然使用131M内存,没有释放。 - - 此时用brk
循环new分配128K x 2048的内存空间,写入脏数据后,循环调用delete释放。top看进程使用,2960字节内存,完全释放。 - - 此时用mmap
设置M_MMAP_THRESHOLD 256k,循环new分配128k * 2048 的内存空间,写入脏数据后,循环调用delete释放,而后调用malloc_trim(0)。top看进程使用,2348字节,完全释放。 - -此时用brk

注:以上测试都是在初次分配内存,此时堆中管理的bin中并没有释放内存的chunk,所以每次分配内存都是brk往上顶(top chunk)或是mmap分配。

结论:
由于测试函数所在的进程不断的通过new从操作系统那里获取小内存,当后面使用delete释放后,glibc内存管理程序并没有立即将手中的内存归还给操作系统,而是交由堆中的bin(空闲内存列表)管理,导致函数调用结束后系统内存依然没有下降的趋势。

其他知识的记录:

1.查看进程的缺页中断次数:
ps -o majflt,minflt -C bin.out
majflt代表major fault,中文名叫大错误,minflt代表minor fault,中文名叫小错误。这两个数值表示一个进程自启动以来所发生的缺页中断的次数。
查看物理内存页使用情况:cat /proc/$PID/smaps,里面详细记录了该进程使用的物理页内存情况,如Private_Dirty、Private_Clean等。

2.在标准的c库提供的malloc和free函数中,底层都是使用brk、mmap和munmap来实现的;

3.mmap系统调用:读写MMAP映射区,相当于读写被映射的文件。本意是将文件当作内存一样读写。相比Read、Write,减少了内存拷贝(Read、Write一个硬盘文件,需要先将数据从内核缓冲区拷贝到应用缓冲区(read),然后再将数据从应用缓冲区拷贝回内核缓冲区(write)。mmap直接将数据从内核缓冲区映拷贝到另一个内核缓冲区),但是被修改的数据从MMAP区同步到磁盘文件上,依赖于系统的页管理算法,默认会慢条斯理得将内容写到磁盘上。另外提供了msync强制同步到磁盘上。

4.linux下查看内存工具
(1)free
(2)top
(3)vmstat

你可能感兴趣的:(#,工作记录)