free not return memory
个人博客:https://rebootcat.com/2020/11/05/free_mem/
内存泄露?
观察到一台机器上的内存使用量在程序启动之后,持续增长,中间没有出现内存恢复。怀疑是不是出现了内存泄露的问题?
然后使用相关的内存分析工具进行了分析:
- gperf
- valgrind (massif)
- 手工标记内存分配释放
上述的分析结果均不能很肯定的得出是否内存泄露的结论。那么问题可能出现在哪里呢?
程序采用 c++ 编写,大量使用了智能指针以及 new/delete,难道内存没有成功释放?亦或是内存释放有什么条件?于是开始怀疑 free 是不是真的释放了内存?
测试
既然怀疑 free 是不是真的释放了内存,此处的释放,是指程序内存占用下降,内存归还给操作系统,那么直接写一个简单的例子进行验证一下。
attention:
测试前,先关闭 swap:
# swapoff -a# free -htotal used free shared buff/cache available
Mem: 3.7G 2.5G 1.1G 8.8M 40M 959M
Swap: 0B 0B 0B
测试1
步骤如下:
- 循环分配大量内存
- block 程序,top 工具观察进程内存占用情况
- 再循环释放所有分配的内存
- block 程序,top 工具观察进程内存占用情况
- 程序退出
上代码:
#include <string>
#include <cstring>
#include <iostream>
#include <vector>
#include <malloc.h>void test(uint32_t num, uint32_t mem_size) {std::cout << "test: mem_size = " << mem_size << " total:" << num * mem_size / 1024.0 / 1024.0 << " MB" << std::endl;std::vector<char*> vec;// 3Gfor (uint32_t i =0; i < num; ++i) {char *ptr = new char[mem_size];vec.push_back(ptr);}std::cout << "allocate memory "<< num * mem_size / 1024.0 / 1024.0 << " MB done" << std::endl;for (auto& ptr : vec) {strncpy(ptr, "abcdefghij", mem_size);}std::cout << "input anything to continue delete all memory..." << std::endl;getchar();for (auto& ptr : vec) {delete ptr;ptr = nullptr;}std::cout << "release memory "<< num * mem_size / 1024.0 / 1024.0 << " MB done" << std::endl;
}int main(int argc, char *argv[]) {uint32_t mem_size = 100;if (argc >=2) {mem_size = std::atoi(argv[1]);}uint32_t num = (uint32_t)2 * 1024 * 1024 * 1024 / mem_size;test(num, mem_size);std::cout << "input anything to exit" << std::endl;getchar();return 0;
}
编译:
g++ mem_test.cc -o mem_test -std=c++11
可以通过参数控制内存分配的大小,默认 100Byte:
./mem_test 100
./mem_test 500
./mem_test 1024
./mem_test 10240
过程就省略了,直接上观察结果:
- 每次测试的虚拟内存大小是类似的,大概在 2G 左右
- 单次分配内存长度为 100 Byte,调用 free 后内存无明显下降
- 单次分配内存长度为 500,1024,10240…,调用 free 后内存迅速下降接近 0%
- 多次测试临界值为 120 Byte
以上测试反应出在不同的情况下, free 的行为有差异,但也说明,调用 free 之后内存是能够立即被释放给操作系统的(只不过有条件)。
那为什么会出现调用 free 之后内存没有被释放(至少看起来是)的情况呢?
测试2
代码不变,还是上面的代码,只不过现在启动两个同样的程序:
- 分别以上述不同的参数启动程序,让程序执行到释放所有内存之后,block 住
- 然后启动第二个程序,用同样的参数
- 观察两个进程是否都能存活
上述有一个条件假设:
total mem: 4G,实际情况可以调整代码里分配内存的总量
过程也省略,直接上观察结果:
- 单次分配内存 100 Byte,启动第二个同样的程序,出现 OOM (先启动这个被 kill)
- 单次分配 500,1024,10240…, 启动第二个同样的程序,不会 OOM
上面的结果和测试1 的结果是吻合的,这能肯定的说明出现 OOM 的场景下,第一个进程的内存虽然完全释放了,但是内存依然被该进程持有,操作系统无法把这部分已经调用 free 的内存重新分配给其他的进程(第二个进程)。
测试3
稍微调整一下上面的代码,分配释放的操作进行两次,也就是上面的 test()
函数调用 2 次。
另外本次使用 valgrind(massif) 进行分析,此次单次内存分配大小为 100 Byte,也就是上面出现无法释放内存的参数。
int main() {
...
test();test(); // call again
...
}
使用 valgrind 进行分析:
valgrind -v --tool=massif --detailed-freq=2 --time-unit=B --main-stacksize=24000000 --max-stackframe=24000000 --threshold=0.1 --massif-out-file=./massif.out ./mem_test
生成的文件 massif.out
使用 massif-visualizer
处理之后得到如下图:
上图就是内存分配的情况,从图中可以很明显的看到在第一次调用 test()
函数时,内存随着分配而增长,随着释放而下降;第二次调用 test()
函数也是同样的情况。
那这幅图能说明什么呢?
第一次调用 test()
后,按照测试2 的情况,内存虽然被释放了,但是内存依然被进程持有,那么不应该出现内存下降的情况,但是从图中看,确实是下降到接近 0 了,那么可以得出一个结论:
test()
至少是没有内存泄露的,即分配的内存,都被释放了(至少标记过释放),也就是没有出现野指针等内存泄露的情况。
那么问题就在于,既然没有内存泄露,那为何内存依然被进程持有?不是已经调用 free 了吗?
glibc malloc/free 实现
glibc malloc 底层调用的是 ptmalloc,这里就不深入 malloc/free 的实现细节了,网上可以找到很多资料。
下图是 32 位程序的虚拟内存空间分布图
原理
向操作系统申请内存涉及到两个系统调用 sbrk 以及 mmap。关于这两个系统调用的区别可以大致这么理解:
- ptmalloc 管理了两块堆内存,所以有可能会在两个地方给用户分配内存
- 这两块堆内存的区别就在于一个可以被循环利用,一个在释放后立即归还操作系统
- ptmalloc 使用 sbrk 来为第一块内存区域 heap 进行内存分配,用户释放之后 ptmalloc 对这块内存进行重新管理利用,进程依然持有这块内存
- ptmalloc 使用 mmap 来为第二块内存区域 sub-heap 进行内存分配,用户释放之后 ptmalloc 立即把这块内存归还给操作系统
- 要分配的内存只有达到一定大小(即 mmap 的阈值),ptmalloc 才会采用 mmap 进行内存分配,否则优先选择 sbrk 分配后被重新管理的内存池
- mmap 的阈值可能是动态调整的,即 ptmalloc 根据自身内存管理情况,动态调整这个阈值
也就是说,ptmalloc 为了性能考虑,采用了两种内存分配策略,也就是管理了两种不同分配方式的堆内存。在分配内存小于一定值时就优先在 ptmalloc 维护的内存池里进行分配,这样避免了直接向操作系统分配内存,减少系统调用次数;如果内存大于一定值时,就直接向操作系统申请内存,并且这段内存在释放之后立即归还操作系统;
这也就能解释上面的几个测试里,当单次分配的内存大小较大时,内存释放后进程内存占用快速下降到 0%;当单次分配的内存大小较小时,内存释放后其实没有归还给操作系统,二是被 ptmalloc 重新回收了,放到了内存池里进行循环利用,所以看到进程内存依然保持较高的占用;
另外关于 ptmalloc 对内存池的管理比较复杂,这里推荐一篇不错的文章可以深度阅读:
glibc内存管理ptmalloc源代码分析
到这里,其实就已经比较明确了,free 之后内存释放情况其实是跟分配的大小有关系的,并且随着程序的运行,内存的持续分配和释放,ptmalloc 的内存池应该能稳定在一定的值,从外面来看,进程的内存占用应该能动态稳定下来。
ptmalloc 的两套分配策略各有优劣,使用内存池可以提高内存分配效率,但是可能出现内存暴涨的情况,但是最终会稳定在一定的值;使用 mmap 的方式分配内存不会出现内存暴涨的情况,释放完之后理解归还操作系统,但降低了内存分配的效率。
修改 malloc 参数
根据上面的讨论,如果想要控制 malloc 的内存分配行为,那么其实是有办法做到的。
我们可以通过下面这个函数来实现:
int mallopt(int param, int value);
https://man7.org/linux/man-pages/man3/mallopt.3.html
可以调整 M_TRIM_THRESHOLD,M_MMAP_THRESHOLD,M_TOP_PAD 和 M_MMAP_MAX 中的任意一个,关闭 mmap 分配阈值动态调整机制。
比如上面的测试1,当单次分配的内存 100 Byte 时,内存释放之后进程内存占用依然较高的情况就能解决:
int main() {mallopt(M_MMAP_THRESHOLD, 64);mallopt(M_TRIM_THRESHOLD, 64);...
}
- M_MMAP_THRESHOLD: mmap 内存分配阈值
- M_RIM_THRESHOLD: mmap 收缩阈值
在 main 函数开始加上上面的两句,调整 mmap 收缩阈值以及内存分配阈值。重新编译运行,发现即使单次分配 100 Byte,内存释放后,进程内存占用也快速下降到 0%。
补充:
int malloc_trim(size_t pad);
可以触发 ptmalloc 对内存的紧缩,即归还一部分内存给操作系统。
总结
进程内存占用较高的情况不一定是内存泄露造成的,可以通过长时间观察内存占用是否能稳定下来进行判断,如果内存占用能实现动态稳定,那么多半程序是没有内存泄露的。
但是如果内存占用过高,对其他的进程产生了干扰,那么可以适当的调整一下 malloc 的参数,控制 malloc 的行为,避免 glibc 内存池过大,影响其他进程的运行。
Blog:
rebootcat.com
email: linuxcode2niki@gmail.com
2020-11-05 于杭州
By 史矛革
相关文章:

成大事必备9种能力
挑战生存的能力:善于在现实中寻找答案 1、摆正心态,敢于面对现实 对于那些不停地抱怨现实恶劣的人来说,不能称心如意的现实,就如同生活的牢笼&a…
懂语言者得天下:NLP凭什么被称为人工智能的掌上明珠?
受访者 | 简仁贤,竹间智能创始人&CEO记者 | 邓晓娟出品 | AI科技大本营(ID:rgznai100)随着技术的发展,大数据、云计算、人工智能、区块链都慢慢地为人熟知。2016 年 Google 推出 AlphaGo,让人工智能走进了大众的视…

[转]SIFT特征提取分析
SIFT(Scale-invariant feature transform)是一种检测局部特征的算法,该算法通过求一幅图中的特征点(interest points,or corner points)及其有关scale 和 orientation 的描述子得到特征并进行图像特征点匹配࿰…

博客大事记之迁移博客到香港主机
个人博客:https://rebootcat.com/2020/11/10/move_blog_hk/ 前言 之前其实已经写过一篇博文: 迁移博客到香港虚拟空间,那为什么又要写这篇博客呢? 上次其实是把我的博客迁移到一个香港的虚拟空间里,但是不到半年的时…
限时福利:腾讯高级专家手把手教你打造 OCR 神器!
OCR,英文全称即 optical characters recognition(光学字符识别),通过服务器把图片上的文字识别出来,以供大家编辑使用,比如进出火车站的时候,已经可以自动识别的身份证;在道路行驶中…

C++与.net的编译方式
C和.Net程序采用了两种不同的编译方式。 通常一个C编写的程序,都是一次编译成二进制的代码,在相应的操作系统平台上直接执行即可。 而.Net程序采用两次编译的方式,用C#,VB.Net等语言写成的程序被编译成IL代码,通过CLR在…

awk (一)
示例文件:[rootorclsrv ~]# catsample Heigh-ho! sing,heigh-ho! unto the green holly: Most friendship isfeigning, most loving mere folly: Then, heigh-ho, theholly!使用感叹号(!) 作为字段分隔符(FS)打印示例数据的第1 个字段:[rootorclsrv~]# …

TCP全连接和半连接的问题探讨
个人博客: https://rebootcat.com/2020/11/14/tcp_accept/ 从何说起 说起 tcp 的连接过程,想必 “3次握手4次挥手”是大家广为熟知的知识,那么关于更细节更底层的连接过程也许就很少人能讲清楚了。 所以本文会先简单回顾一下 tcp 的 3次握手…

[转] ASP.NET MVC3 路由和多数据集的返回
1.ASP.NET MVC3 中的路由 同前边一样本篇并不会过多的介绍理论知识,我们在Global.asax.cs文件中可以看到如下代码: routes.MapRoute("Default", // Route name"{controller}/{action}/{id}", // URL with parametersnew { controlle…
给Python代码加上酷炫进度条的几种姿势
作者 | 刘早起来源 | 早起Python(ID: zaoqi-python)大家好,在下载某些文件的时候你一定会不时盯着进度条,在写代码的时候使用进度条可以便捷的观察任务处理情况,除了使用print来打印之外,今天本文就介绍几种…

(转)mongodb分片
本文转载自:http://www.cnblogs.com/huangxincheng/archive/2012/03/07/2383284.html 在mongodb里面存在另一种集群,就是分片技术,跟sql server的表分区类似,我们知道当数据量达到T级别的时候,我们的磁盘,内…

深入浅出paxos
原文 https://rebootcat.com/2020/12/05/paxos/
Uber 前无人驾驶工程师告诉你,国内无人驾驶之路还要走多久?
受访者 | Graviti 创始人&CEO 崔运凯记者 | Aholiab,编辑 | Carol出品 | AI科技大本营(ID:rgznai100)经过数年的发展,现在的人们谈到“AI”已经不再像过去一般感到遥不可及。但 AI 在国内发挥的作用仍然只是冰山一角ÿ…

oracle服务
OracleOraDb11g_home1ClrAgent服务,在网上查找到了资料 http://download.oracle.com/docs/cd/B19306_01/win.102/b14306/install.htm Configuring Extproc Agent Using Windows Service As part of Oracle Database Extensions for .NET installation, a Windows se…

B00009 C语言分割字符串库函数strtok
切割字符串是常用的处理。 这里给出一个使用函数strtok切割字符串的例子。 使用C语言的库函数strtok来切割字符串的好处在于,可以指定任意字符作为分隔符来切割单词。使用该函数,切割字符串的分隔符可以同时指定多个,放在一个字符串数组中。 …

WEB打印大全
1、控制"纵打"、 横打”和“页面的边距。 (1)<script defer> function SetPrintSettings() { // -- advanced features factory.printing.SetMarginMeasure(2) // measure margins in inches factory.SetPageRange(false, 1, 3) // ne…
漫画 | 程序媛小姐姐带你一次了解什么是排序算法
来源 | 小齐本齐封图 | CSDN 付费下载自视觉中国插入排序借用《算法导论》里的例子,就是我们打牌的时候,每新拿一张牌都会把它按顺序插入,这,其实就是插入排序。齐姐声明:虽然我们用打牌的例子,但是可不能学…

POJ 1207 The 3n + 1 problem
题目链接:http://poj.org/problem?id1207 题目大意:给你一个数x,规定一个函数F(x),如果x为1则F(x)1,否则如果x是偶数,F(x)F(x/2),x为奇数F(x)F(3*x1)计算给定x到变换到1的步数。 注意点&#x…

PopupWindow响应返回键的问题
假设情景是这样的:在一个Activity中弹出一个PopupWindow,要求在按返回键时关闭该PopupWindow。 如果该PopupWindow是无焦点的(默认情况),那么可以在Activity中响应返回键(onBackPressed)&#x…

Unix / Linux世界里的4-2-1
Unix / Linux世界里的4-2-1 在Unix / Linux世界里,4代表可读( r ),2代表可写入 ( w ),1代表可执行 ( x ) 如果拥有7 421 的权限,即代表这个人可以对档案完全控制。 以0777为例: 去掉0,第一个7代表着拥有者…
深度学习概述:NLP vs CNN
作者 | Manish Kuwar译者 | 苏本如,责编 | 郭芮头图 | CSDN 下载自视觉中国出品 | CSDN(ID:CSDNnews)以下为译文:当今,人工智能已经不仅仅是一个技术术语了。这项技术在过去十年的时间内几乎将其影响扩展到…

oracle 求A中不存在于B的记录
oracle 求A中不存在于B的记录 select * from a minus select * from b 是求A中不存在于B的记录select * from a union select * from b 是求A和B的DISTINCT的并集select * from a union all select * from b 是求A和B的冗余并集那么A和B的交集是什么函数来的?交集是 INTERSE…

正则表达式grep、egrep--already
第一式 grep是什么 #man grepgrep(global search regular expression(RE)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。UNIX的grep家族包括grep、egrep和fgrep。egrep和fgrep的命令…
万字长文综述目标检测领域,你要的都在这里
来源 | AI专栏(ID: pursue-Y-future)目标检测是计算机视觉中的一个重要问题,近年来传统检测方法已难以满足人们对目标检测效果的要求,随着深度学习在图像分类任务上取得巨大进展,基于深度学习的目标检测算法逐渐成为主…

ASP.net随机数应用实例
家可能都用过Chinaren的校友录,不久前它的留言簿上加了一个防止灌水的方法,就是系统每次产生一个由随机的数字和字母组成的图片,每次留言必须正确地输入这些随机产生的字符,否则不能添加留言。这是一个很好的防止恶意攻击的方法&a…

PreferenceActivity是什么?
我们看到Android系统本身就大量用到了PreferenceActivity来对系统进行信息配置和管理,那么它是怎么保存数据的呢,如何创建PrefenceActivity的呢?创建Android项目,并添加一个pref.xml文件(先建一个xml名的Folder)。注意,这次选择的…
坑系列 --- 时间和空间的平衡
这是坑系列的最后一弹了,这篇文章非常长,希望你能看完,要是看完有很酣畅的感觉就最好了。这一篇的坑主要来说说架构中时间和空间的平衡吧,这里的时间指代比较广,可能是开发时间,但大部分指的是执行时间&…

C#中调用Windows API的要点
在.Net Framework SDK文档中,关于调用Windows API的指示比较零散,并且其中稍全面一点的是针对Visual Basic .net讲述的。本文将C#中调用API的要点汇集如下,希望给未在C#中使用过API的朋友一点帮助。另外如果安装了Visual Studio .net的话&…
线上直播丨Hinton等6位图灵奖得主、百余位顶级学者邀你群聊AI
Geoffrey Hinton等6位图灵奖得主亲临,百余位顶级学者邀请你加入群聊「2020北京智源大会」,深入系统探讨「人工智能的下一个十年」。自2009年深度学习崛起以来,第三波人工智能浪潮席卷全球,推动了新一波技术革命。在这波澜壮阔的11…

ServerSocket
ServerScoket 这个类用于与 Socket 进行通信。 在实例化ServerSocket 的时候,服务器相当于已经开始了,但是还需要通过socket来accept (socket serverSocket.accept())以使服务器选择性与某一Client进行连接。如果有指定了允许连接…