map容器clear操作不会释放内存?

一,map容器clear操作不会释放内存?

当第一次听到这个说法的时候确实有点惊讶。一直记得map容器底层红黑树会自动析构节点,并释放内存。在同事进行了代码验证,并百度了答案后,我也变得不确定起来了。

只有再次看了一遍《STL源码剖析》。希望最终能够回答这个问题。

最后发现这个问题即对又不对。为什么呢?看后面的分析。

二,map的clear操作

2.1 clear源码

Void clear(){t.clear();}; //P241页《STL源码剖析》

由于关联容器底层是红黑树实现,所以map的clear也是调用的红黑树的clear函数。

那么我们接下来再看看红黑树的clear操作。

在开发机中可以找到文件stl_tree.h,erase操作的源码如下:

该操作中核心调用的destroy_node分成两步:

1,先析构元素,2,然后释放内存。(destroy_node操作与《STL源码剖析》书中的写法有一点出入,不过本质是一样的)

其中步骤2释放内存操作代码如下:

这里的deallocate操作就是STL中封装的全局内存分配操作函数。

读到这里就比较清楚了,map容器的erase以及clear操作底层都是调用的全局函数deallocate进行内存释放操作。

2.2 deallocate源码分析

Std::alloc为内存配置器,具体又分为一级和二级配置器。负责为容器提供内存空间操作,提供了常用的两个操作:allocate分配内存,deallocate释放内存。

一级配置器与二级配置器的区别:

如果申请的内存块大于128字节,则STL使用一级配置器。Allocate底层调用malloc操作,deallocate调用free操作。

如果申请的内存块小于128字节,则使用二级配置器。该配置器具体实现类似于一个内存池。减少频繁系统,提高内存分配效率。缺点:为了维护小块内存,需要额外的空间来进行管理。

2.3 小结

到这里就可以有一个小结论。

1,当map中的元素占用内存大小总和小于128字节时,则erase或者clear操作确实不会释放内存(包括虚拟和物理内存)。

2,当元素对象大于或等于128字节,则直接系统调用malloc或者free进行内存的分配(malloc只是分配虚拟内存)

3,只有当对分配的内存区域初始化时,操作系统才会分配物理内存,产生minflt小缺页。

三,STL中常用容器内存操作总结

3.1 vector容器

3.1.1 空容器

容量大小为0。

vector<int> v;

cout<<"size: "<<v.size()<<", capacity: "<<v.capacity()<<endl;//0 0

3.1.2 写入操作(push_back等)

容量变化:

如果是对容量为0的容器进行一次push_back操作,则容器大小变为1。

如果该容器容量已满,则会对容器容量扩容一倍,并把旧容器的元素拷贝至新内存中。

元素构造:

如果容器没有容量,则分两步完成操作:先allocate分配内存,然后construct构造元素。

如果有足够容量,则只调用construct构造元素即可。

3.1.3删除操作(pop_back,erase,clear等)

只调用析构函数destroy,并不会进行内存的释放。即容器的capacity并不会变化。

3.1.4 析构函数

可以看到,即调用了析构函数,也调用了内存释放函数。

所以常常vector或者string在进行一系列操作后,容量变得非常大,那么可以通过下面的技巧进行容量的缩减。

vector<int> v;

v.push_back(1);

v.push_back(2);

……

vector<int>().swap(v); // 当clear()用。和临时对象交换,交换后临时对象消失。真正将v释放掉。

// or: v.swap(vector<int>());

3.2 list操作

3.2.1 empty操作

Empty操作性能好于size()==0。《Effective STL》

3.2.2 写入操作(insert)

两步完成:1,分配内存。2,构造node,并进行赋值。

3.2.3 删除操作(pop_back,erase等)

分别调用析构函数,内存释放函数完成操作。

3.3 deque双端队列

分段连续,随机迭代器。没有容量的概念

3.3.1 初始状态

至少存在一个缓冲区。

3.3.2 写入操作

如果分段缓冲区还有空间,则直接调用constuct操作。否则分配内存,分段缓冲区,然后构造该元素。

3.3.3 删除操作

如果删除的一段缓冲区还有数据,则只析构对象,并不释放内存。

如果删除后,该段缓冲区没有数据,则析构元素,并释放内存。

Clear或者erase操作后,会保留一个缓存区,只对其析构元素。其他缓冲区即析构函数,也释放内存。

3.4 关联容器之map

关联容器都是红黑树(hash_xx除外)。具有较高的查找和插入效率,元素有序。

3.4.1 写入操作

Insert操作会先分配内存,初始化树node节点,node节点元素类型为pair。

3.4.2 下标操作

Map容器的下标操作既可以作为左值,也可以作为右值。因为其返回的是引用。

也是一个效率非常低的函数,尽量少用。

另外一个副作用是:当key值在map中不存在时,会自动在map中插入一个key,value为默认值的元素。造成map结构增大。

如下代码:

map<int, int> map_int; cout<<"size "<<map_int.size()<<endl;//0 for(int i = 0; i != 10; i++ ) { int j = map_int[i]; }

cout<<"size "<<map_int.size()<<endl;//10

3.4.3 删除操作

Erase等删除操作,即会析构元素,也会释放元素占用的内存。

四,操作系统内存分配

通过上面的分析,基本上弄清楚了STL容器对内存的分配和释放原理。

最终是调用malloc或者free来进行内存分配的。

参考《十问 Linux 虚拟内存管理 (glibc)》http://km.oa.com/group/17613/articles/show/135448?kmref=search

4.1 glibc内存分配原理

1,当分配块小于128K时,通过brk系统调用控制堆顶往高方向变化。

2,当分配块大于128K时,则通过mmap2和munmap来进行内存的分配和释放的。

4.2

频繁minflt

因此,当频繁分配大内存(大于128K)时,则会调用mmap。Free操作时,会立即调用munmap把该物理和虚拟内存归还给操作系统。会有较高的系统耗时以及minflt。

常常通过命令pidstat –p pid –r 1即可查看。

通过strace –p pid –T –ttt –c 命令,可以看到大量的mmap操作。

这种情况下,常常可以禁用mmap或者提高128K这个门槛来减少mmap系统调用。

Spp框架在实现中有如下代码:

mallopt(M_MMAP_THRESHOLD, 1024*1024); // 1MB,防止频繁mmap,大于1M,则使用mmap分配内存

mallopt(M_TRIM_THRESHOLD, 8*1024*1024); // 8MB,防止频繁brk,大于8M,才使用brk归还内存

这里需要注意的是,通过M_MMAP_THRESHOLD设置后,大于1M肯定是mmap分配内存,但是当分配内存小于1M时,还是可能通过mmap分配内存。具体见描述info mallopt

另外一种解决进程有较大minflt以及频繁系统调用mmap分配内存策略。完全禁止mmap操作以及禁止内存紧缩。

mallopt(M_MMAP_MAX, 0); // 禁止malloc调用mmap分配内存

mallopt(M_TRIM_THRESHOLD, -1); // 禁止内存紧缩

带来的缺点:

会存在进程持有内存不释放的现象,进程占用内存较高,造成假性内存泄漏。

4.3 内存紧缩

当通过brk分配内存时,堆连续空闲内存大于128K,则内存紧缩,将物理内存归还给操作系统。

对于持续分配较多小内存的程序时,这也会降低内存的使用率。因此可以通过调节紧缩大小或者禁用紧缩策略来解决。

方法1,

mallopt(M_TRIM_THRESHOLD, 8*1024*1024); // 8MB,防止频繁brk,大于8M,才使用brk归还内存

方法2,

mallopt(M_TRIM_THRESHOLD, -1); // 禁止内存紧缩

五,结论

1,map的删除操作,其本身肯定会释放元素占用的内存。

2,具体是否释放进程虚拟地址空间和物理内存,与内存gblic分配策略方式有关,而不是map本身的特性。

3,malloc操作只是分配虚拟地址空间,只有当对该内存区域有写操作时,才会产生minflt,分配物理内存。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Linux Python 加油站

五个python常用运维脚本面试题实例

来源:马哥教育原文作者:chengxuyuan 链接:https://mp.weixin.qq.com/s/nahDVL6aiMQ2vp85wo6nNw一、用P...

31410
来自专栏大数据风控

Python中时间格式数据的处理

1、时间转换 时间转换是指字符型的时间格式数据,转换成为时间型数据的过程。 一般从csv导入过来的文件,时间都保存为字符型格式的,需要转换。 时间转换函数: d...

399100
来自专栏chenssy

【死磕Java并发】—–Java内存模型之总结

经过四篇博客阐述,我相信各位对Java内存模型有了最基本认识了,下面LZ就做一个比较简单的总结。 总结 JMM规定了线程的工作内存和主内存的交互关系,以及线程之...

37080
来自专栏Java面试笔试题

什么是ORM?

对象关系映射(Object-Relational Mapping,简称ORM)是一种为了解决程序的面向对象模型与数据库的关系模型互不匹配问题的技术;简单的说,O...

18520
来自专栏coding

vim实用笔记

20120
来自专栏蓝天

Dash与Bash的语法区别

本文系转载,原文URL为:http://www.igigo.net/archives/169

16020
来自专栏DannyHoo的专栏

block传值

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

11320
来自专栏张戈的专栏

Linux:sed命令详解

1. 简介 sed 是非交互式的编辑器。它不会修改文件,除非使用 shell 重定向来保存结果。默认情况下,所有的输出行都被打印到屏幕上。 ? sed 编辑器逐...

53660
来自专栏互扯程序

java中的内存模型

现在是资源共享的时代,同样也是知识分享的时代,如果你觉得本文能学到知识,请把知识与别人分享。

18240
来自专栏vue

C++项目中采用CLR的方式调用C#编写的dll

1、注意事项:在编写C#DLL类库时,最好不要出现相同的命名空间,否则在C++中调用可能会出现编译错误。 2、将C#的源码生成的“dll”文件复制到C++项目中...

32430

扫码关注云+社区

领取腾讯云代金券