内存分配原理
康 林 2011-1-12
W问:“K,知道,stl的list直接clear后怎样可以释放内存没!”
K答:“stl 中 list 的 clear 方法会自己释放自己分配的节点内存,但如果数据是用户用 new 产生的,那用户需要负责释放。”
W说:“我说的就是list自己分配的节点内存如何强行释放,它没有自己释放。刚才找了下,3.3以前的gcc会自己释放,3.4以后的就不会释放!”
K说:“有这事?它会释放它自己分配的节点内存。这点是STL标准,不会变的。我先查下。”
W说:“恩!至少我看我代码中clear是不释放了,帮我找找怎么让他强行释放!”
从标准来说,clear肯定会释放它自己分配的内存的。为了说服他,就开始查找相关的标准。
http://www.cplusplus.com/reference/stl/list/clear/
All the elements in the list container are dropped: their destructors are called, and then they are removed from the list container, leaving it with a size of 0.
写测试程序:
#include <iostream>
#include <list>
int main()
{
std::list<int> lstData;
sleep(10);
for(int i = 0; i < 500000; i++)
{
std::cout << i << std::endl;
lstData.push_back(i);
}
std::cout << "clear:" << lstData.size() << std::endl;
lstData.clear();
std::cout << "end:" << lstData.size() << std::endl;
sleep(10);
while(1)
std::cout << "ok" << std::endl;
}
环境:XP,VS2005,运行,一切正常,内存被释放。
看客:“你怎么查看内存的?”
“……,哪里凉快你就去哪里歇着吧!”
Windows下一切正常,再在linux下运行下看是否正常。环境:
操作系统:
Linux 2.6.16.60-0.21-smp #1 SMP Tue May 6 12:41:02 UTC 2008 x86_64 x86_64 x86_64 GNU/Linux
g++ 版本:
Target: x86_64-suse-linux
Configured with: ../configure --enable-threads=posix --prefix=/usr --with-local-prefix=/usr/local --infodir=/usr/share/info --mandir=/usr/share/man --libdir=/usr/lib64 --libexecdir=/usr/lib64 --enable-languages=c,c++,objc,fortran,obj-c++,java,ada --enable-checking=release --with-gxx-include-dir=/usr/include/c++/4.1.2 --enable-ssp --disable-libssp --disable-libgcj --with-slibdir=/lib64 --with-system-zlib --enable-shared --enable-__cxa_atexit --enable-libstdcxx-allocator=new --program-suffix= --enable-version-specific-runtime-libs --without-system-libunwind --with-cpu=generic --host=x86_64-suse-linux
Thread model: posix
gcc version 4.1.2 20070115 (SUSE Linux)
运行。发现top查看到的内存果然如W所说没有释放!怎么会这样呢?就开始分析内存分配过程。查看STL的代码。
看客:“STL代码那么多,你如何知道看哪个文件?”
K答:“查看代码的方法有很多种,我常用的是调试跟踪法。”
看客:“没用过。”
K答:“那你先去了解下gdb的调试方法,再回来看。”
/**
* Erases all the elements. Note that this function only erases
* the elements, and that if the elements themselves are
* pointers, the pointed-to memory is not touched in any way.
* Managing the pointer is the user's responsibilty.
*/
void
clear()
{
_Base::_M_clear();
_Base::_M_init();
}
注释写得很清楚:它会释放自己分配的内存,但用户自己分配的数据内存要用户自己处理。我们继续看它的实现:
template<typename _Tp, typename _Alloc>
void
_List_base<_Tp, _Alloc>::
_M_clear()
{
typedef _List_node<_Tp> _Node;
_Node* __cur = static_cast<_Node*>(this->_M_impl._M_node._M_next);
while (__cur != &this->_M_impl._M_node)
{
_Node* __tmp = __cur;
__cur = static_cast<_Node*>(__cur->_M_next);
_M_get_Tp_allocator().destroy(&__tmp->_M_data);
_M_put_node(__tmp);
}
}
这里运用了策略模式进行内存的分配管理。由于标准STL默认的内存分配策略是直接调用 delete。所以这里没有进行缓存(你仔细想想,确实也没有缓存的必要)。但是如果用户想要STL进行缓存内存的话,他可以根据自己的需求定义内存分配策略。
继续跟踪,确实STL默认的内存分配策略是直接调用 delete。
// __p is not permitted to be a null pointer.
void
deallocate(pointer __p, size_type)
{ ::operator delete(__p); }
到此,可以证明:标准STL会释放自己分配的节点内存,并且不会进行缓存。
那么为什么与实验结果不符呢?我们把目光放到了 delete 上,继续分析:Windows下运行正常,linux下确实没有释放。是不是操作系统内存分配策略上不同?从操作系统原理可知,是由操作系统内存分配置策略不同引起的,具体的请参《操作系统原理》考清华大学。或者:http://blog.163.com/prevBlogPerma.do?host=holyrain1314&srl=1001141352010103011524719&mode=prev
继续我们linux下的分析……
C++ delete 操作最终调用了 C库中的 glibc 中的 malloc 函数。下面是我环境的依赖关系:
libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x00002b893a169000)
libm.so.6 => /lib64/libm.so.6 (0x00002b893a367000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00002b893a4bc000)
libc.so.6 => /lib64/libc.so.6 (0x00002b893a5ca000)
/lib64/ld-linux-x86-64.so.2 (0x00002b893a04d000)
new和delete 是在 libstdc++中实现的。声明在 #include <new>。
从标准C库中的内存分配库中可以得到些malloc分配的信息。详细参见:#include <malloc.h>
修改上面的代码,把这些信息打印出来。
#include <iostream>
#include <list>
#include <malloc.h>
int main()
{
std::list<int> lstData;
sleep(15);
std::cout << "start" << std::endl;
malloc_stats();
for(int i = 0; i < 50000000; i++)
{
// std::cout << i << std::endl;
lstData.push_back(i);
}
std::cout << "clear:" << lstData.size() << std::endl;
malloc_stats();
lstData.clear();
malloc_stats();
std::cout << "end:" << lstData.size() << std::endl;
sleep(10);
while(1)
std::cout << "ok" << std::endl;
}
运行结果:
Arena 0:
system bytes = 0
in use bytes = 0
Total (incl. mmap):
system bytes = 0
in use bytes = 0
max mmap regions = 0
max mmap bytes = 0
clear:50000000
Arena 0:
system bytes = 1600118784
in use bytes = 1600000000
Total (incl. mmap):
system bytes = 1600118784
in use bytes = 1600000000
max mmap regions = 0
max mmap bytes = 0
Arena 0:
system bytes = 1600118784
in use bytes = 0
Total (incl. mmap):
system bytes = 1600118784
in use bytes = 0
max mmap regions = 0
max mmap bytes = 0
从上面可以看出 in use bytes = 0,system bytes = 1600118784系统没有释放。看来内存管理是在 glibc 中做的。我们继续跟踪代码……
由于现有glibc库不为调试版本,所以前面跟踪法用不了了。我采用了逆向跟踪法。继续分析……
我又写了一个测试程序:
void main()
{
std::cout << "new" << std::endl;
char *p = new char;
if(p)
{
std::cout << "delete" << std::endl;
delete p;
}
}
这个程序很简单,目的就是为了跟踪低层的系统调用。
write(1, "new/n", 4new
) = 4
brk(0) = 0x503000
brk(0x524000) = 0x524000
write(1, "delete/n", 7delete
) = 7
当分配置的内存比较小时,调用了系统调用 brk
把char *p = new char;改成char *p = new char[1024*1024;得到下面结果:
write(1, "new/n", 4new
) = 4
mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2ac861bb1000
write(1, "delete/n", 7delete
) = 7
当需要分配的内存较大时,直接调用了系统调用 mmap。而delete时没有调用系统调用。这正好与malloc的使用说明中的注释一致。
Normally, malloc() allocates memory from the heap, and adjusts the size of the heap as required, using sbrk(2). When allocating blocks of memory larger than MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory as a private anonymous mapping using mmap(2). MMAP_THRESHOLD is 128 kB by default, but is adjustable using mallopt(3). Allocations performed using mmap(2) are unaffected by the RLIMIT_DATA resource limit (see getrlimit(2)).
从这两次实验可以得知new最后调用的系统调用是 brk 和 mmap。下载一份 glibc 的源码。进行分析……
mmap <—— GC_unix_get_mem <—— GET_MEM<——GC_scratch_alloc <——alloc_hdr<——GC_init_headers<——GC_init_inner<—— GC_alloc_large <——GC_generic_malloc <——GENERAL_MALLOC <——GC_malloc <——GC_MALLOC<——operator new
GC_mallo<——REDIRECT_MALLOC<——malloc
在GC_generic_malloc判断是申请内存是否大值,如果是大于则按上面的线路调系统调用 mmap。如果小于,则最终会调用brk。
有人说new实际上是调用了glibc的malloc。但是从这个版本中的情况看不是这样的。在这个版本中,new和malloc都调用了宏,然后再由宏定义到GC_malloc。(也许以前的版本是直接调用malloc的吧。这个是我推测的。但现在这个版本的扩展性更好,更加合理。)准确的讲,现在的new和malloc没有关系,一个是glibc提供给C++的接口,一个是提供给C的接口。但它两的具体实现是一样的(都是通过调用内部接口GC_malloc实现的)。
munmap<——GC_unmap_gap<——GC_merge_unmapped<——GC_alloc_large<——GC_generic_malloc …… GC_freehblk <——GC_free<——REDIRECT_FREE<——free
brk和sbrk主要的工作是实现虚拟内存到内存的映射.在GNU C中,内存分配是这样的:
每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这一块分配的。如果这块空间不够,malloc函数族(realloc,calloc等)就调用sbrk函数将数据段的下界移动,sbrk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。(参见linux内核情景分析)
Brk系统调用原码分析:http://wenku.baidu.com/view/3928babd960590c69ec37600.html
linux中的物理地址和虚拟地址http://blog.163.com/prevBlogPerma.do?host=hujianjust&srl=72455072201042795435529&mode=prev
#include <iostream>
#include <list>
#include <malloc.h>
int main()
{
long n = 30240;
for(long i = 0;i < n;i++)
{
char *p = new char[1024*1024];
if(!p)
std::cout << "fail new" << std::endl;
}
while(1)
{
std::string a;
std::cout << "enter:";
std::cin >> a;
malloc_stats();
}
}
这段程序可以正常运行,成功分配了所需要的内存。
#include <iostream>
#include <list>
#include <malloc.h>
int main()
{
long n = 30240;
for(long i = 0;i < n;i++)
{
char *p = new char[1024*1024];
if(!p)
std::cout << "fail new" << std::endl;
else
{
for(long j = 0; j < 1024*1024;j++)
p[j] ='a';
}
}
while(1)
{
std::string a;
std::cout << "enter:";
std::cin >> a;
malloc_stats();
}
}
加了上面程序段后,当内存消耗到快接近物理内存大小时,程序就被操作系统给kill了。
从上面这两段程序运行的结果可以看出:mmap向内核申请内存时,并没有真正分配,只有当写入时,才真正进行虚拟内存到物理内存的映射操作。