知识点参考IBM
内存管理是计算机编程最基本的领域之一。在许多脚本语言中,您不必担心如何管理内存,但这并不会使内存管理变得不那么重要。了解内存管理器的能力和局限对于有效编程至关重要。
如果事先没有计算程序的每个部分需要多少内存,空间有限且内存需求不同,可以从下面思路考虑:
实现这些要求的库称为分配器,因为它们负责分配和释放内存。程序越动态,内存管理就越成问题,内存分配器的选择就越重要。
可以从这几个方面看:
管理内存的不同方法,它们的优点和缺点,以及它们最佳工作的情况。
C编程语言提供了两个函数来满足我们的三个要求:
malloc:这将分配给定数量的字节并返回指向它们的指针。如果没有足够的可用内存,则返回空指针。
free:这需要一个指向由其分配的内存段的指针malloc,并将其返回供程序或操作系统以后使用(实际上,某些malloc实现只能将内存返回给程序,而不是返回给操作系统)。
知识点简易了解:
RAM 就是 内部存储器 也就是平时说的 内存
ROM 就是 外部存储器 也就是 硬盘 包括 U盘 移动硬盘 光盘 这些 都叫ROM
ROM(硬盘) 就是储存数据用的 但这些数据 要想通过处理器来运行 就需要从ROM调到RAM中
就是把数据从硬盘调到内存中 处理器才能处理这些数据 处理器无法直接处理ROM(硬盘)中的数据 因为ROM速度太慢 跟不上处理器的速度 所以需要把数据放到RAM中 RAM比ROM快好多
物理内存 指的 就是RAM(内存) 就是你电脑主版上插的内存条 插1G 你就有1G 插2G 你就有2G物理内存
但是有时候 物理内存不太够用 怎么办呢 这时 就用到了虚拟内存
虚拟内存 就是在ROM (硬盘)中 划分一部分区域 来作为临时的RAM(内存)
RAM(内存)都比较小 只有几G 所以放不了太多东西 而电脑ROM(硬盘)一般都有几百G 比较大
把RAM(内存)中的一些暂时不用的数据 暂时挪到ROM(硬盘)中 这样 RAM就腾出了空间 来处理其他数据
虚拟内存毕竟是占用ROM(硬盘) 所以速度还是慢 比不上RAM 所以虽然具有内存的部分功能 但不是真正的RAM(内存) 所以才称它为虚拟
物理和虚拟内存
要了解如何在程序中分配内存,首先需要了解如何从操作系统将内存分配给程序。计算机上的每个进程都认为它可以访问您的所有物理内存。显然,由于您同时运行多个程序,因此每个进程都不能拥有所有内存.那么进程正在使用虚拟内存会发生什么?
举个例子,假设您的程序正在访问内存地址629.但是,虚拟内存系统并不一定将它存储在RAM位置629.事实上,它甚至可能不在RAM中 - 它甚至可能如果您的物理内存已满,则已移至磁盘!由于地址不一定反映内存所在的物理位置,因此称为虚拟内存。操作系统维护一个虚拟地址到物理地址转换表,以便计算机硬件可以正确响应地址请求。并且,如果地址在磁盘而不是RAM中,操作系统将暂时停止进程,将其他内存卸载到磁盘,从磁盘加载请求的内存,然后重新启动进程。
看不懂可以看图
在32位x86系统上,每个进程可以访问4 GB内存。现在,大多数人的系统上没有4 GB的内存,即使你包含交换,每个进程必须少于4 GB 。因此,当进程加载时,它会获得内存的初始分配,直到某个地址,称为系统中断。过去是未映射的内存 - 在RAM或磁盘上没有分配相应物理位置的内存。因此,如果进程从其初始分配中耗尽内存,则必须请求操作系统“映射”更多内存。(映射是一对一对应的数学术语 - 当虚拟地址具有相应的物理位置以存储时,内存被“映射”。)
基于UNIX的系统有两个基本系统调用,映射在额外的内存中:
brk:brk()是一个非常简单的系统调用。还记得系统中断,即进程映射内存边缘的位置吗?brk()只需向前或向后移动该位置,即可向进程添加内存或从进程中删除内存。
mmap:mmap()或“内存映射”之类似brk()但更灵活。首先,它可以在任何地方映射内存,而不仅仅是在流程结束时。其次,它不仅可以将虚拟地址映射到物理RAM或交换,还可以将它们映射到文件和文件位置,以便读取和写入内存地址将读取和写入文件的数据。然而,在这里,我们只关心mmap将映射的RAM添加到我们的进程的能力。munmap()反过来mmap()。
如果方便的话,可以动手操作下,实现一个简单的分配器:
/* Include the sbrk function */
#include
int has_initialized = 0;
void *managed_memory_start;
void *last_valid_address;
void malloc_init()
{
/* grab the last valid address from the OS */
last_valid_address = sbrk(0);
/* we don't have any memory to manage yet, so
*just set the beginning to be last_valid_address
*/
managed_memory_start = last_valid_address;
/* Okay, we're initialized and ready to go */
has_initialized = 1;
}
struct mem_control_block {
int is_available;
int size;
};
void free(void *firstbyte) {
struct mem_control_block *mcb;
/* Backup from the given pointer to find the
* mem_control_block
*/
mcb = firstbyte - sizeof(struct mem_control_block);
/* Mark the block as being available */
mcb->is_available = 1;
/* That's It! We're done. */
return;
}
void *malloc(long numbytes) {
/* Holds where we are looking in memory */
void *current_location;
/* This is the same as current_location, but cast to a
* memory_control_block
*/
struct mem_control_block *current_location_mcb;
/* This is the memory location we will return. It will
* be set to 0 until we find something suitable
*/
void *memory_location;
/* Initialize if we haven't already done so */
if(! has_initialized) {
malloc_init();
}
/* The memory we search for has to include the memory
* control block, but the user of malloc doesn't need
* to know this, so we'll just add it in for them.
*/
numbytes = numbytes + sizeof(struct mem_control_block);
/* Set memory_location to 0 until we find a suitable
* location
*/
memory_location = 0;
/* Begin searching at the start of managed memory */
current_location = managed_memory_start;
/* Keep going until we have searched all allocated space */
while(current_location != last_valid_address)
{
/* current_location and current_location_mcb point
* to the same address. However, current_location_mcb
* is of the correct type so we can use it as a struct.
* current_location is a void pointer so we can use it
* to calculate addresses.
*/
current_location_mcb =
(struct mem_control_block *)current_location;
if(current_location_mcb->is_available)
{
if(current_location_mcb->size >= numbytes)
{
/* Woohoo! We've found an open,
* appropriately-size location.
*/
/* It is no longer available */
current_location_mcb->is_available = 0;
/* We own it */
memory_location = current_location;
/* Leave the loop */
break;
}
}
/* If we made it here, it's because the Current memory
* block not suitable, move to the next one
*/
current_location = current_location +
current_location_mcb->size;
}
/* If we still don't have a valid location, we'll
* have to ask the operating system for more memory
*/
if(! memory_location)
{
/* Move the program break numbytes further */
sbrk(numbytes);
/* The new memory will be where the last valid
* address left off
*/
memory_location = last_valid_address;
/* We'll move the last valid address forward
* numbytes
*/
last_valid_address = last_valid_address + numbytes;
/* We need to initialize the mem_control_block */
current_location_mcb = memory_location;
current_location_mcb->is_available = 0;
current_location_mcb->size = numbytes;
}
/* Now, no matter what (well, except for error conditions),
* memory_location has the address of the memory, including
* the mem_control_block
*/
/* Move the pointer past the mem_control_block */
memory_location = memory_location + sizeof(struct mem_control_block);
/* Return the pointer */
return memory_location;
}
大多数操作系统上的内存分配由两个简单的函数处理:
void *malloc(long numbytes):这分配numbytes内存并返回指向第一个字节的指针。
void free(void *firstbyte):给定一个前一个返回的指针malloc,这会给出分配回进程的“可用空间”的空间。
malloc_init将是我们初始化内存分配器的函数。它做了三件事:将我们的分配器标记为初始化,找到系统上的最后一个有效内存地址,并设置指向我们的托管内存开头的指针。这三个变量是全局变量:
简单分配器的全局变量
int has_initialized = 0; void *managed_memory_start; void *last_valid_address;
如上所述,映射内存的边缘 - 最后一个有效地址 - 通常称为系统中断或当前中断。在许多UNIX®系统上,要查找当前系统中断,请使用该功能sbrk(0)。sbrk将当前系统中断移动其参数中的字节数,然后返回新的系统中断。使用参数调用它0只会返回当前的中断。这是我们的malloc初始化代码,它找到当前的中断并初始化我们的变量
分配器初始化函数
/* Include the sbrk function */ #include <unistd.h> void malloc_init() { /* grab the last valid address from the OS */ last_valid_address = sbrk(0); /* we don't have any memory to manage yet, so *just set the beginning to be last_valid_address */ managed_memory_start = last_valid_address; /* Okay, we're initialized and ready to go */ has_initialized = 1; }
现在,为了正确管理内存,我们需要能够跟踪我们分配和解除分配的内容。我们需要free在调用块之后将块标记为未使用,并且在调用时能够找到未使用的块malloc。因此,返回的每一块内存malloc的开头都会在开头有这个结构:
内存控制块结构定义
struct mem_control_block { int is_available; int size; };
接下来看 如何实现 分配和释放
我们要释放内存的唯一办法是获取我们给出的指针,备份sizeof(struct mem_control_block)字节,并将其标记为可用。这是代码:
void free(void *firstbyte) { struct mem_control_block *mcb; /* Backup from the given pointer to find the * mem_control_block */ mcb = firstbyte - sizeof(struct mem_control_block); /* Mark the block as being available */ mcb->is_available = 1; /* That's It! We're done. */ return; }
在这个分配器中,使用一种非常简单的机制,可以在恒定时间内完成释放内存。分配内存稍微困难一些。以下是算法的概要:
主分配器的伪代码
1. If our allocator has not been initialized, initialize it. 2. Add sizeof(struct mem_control_block) to the size requested. 3. Start at managed_memory_start. 4. Are we at last_valid address? 5. If we are: A. We didn't find any existing space that was large enough -- ask the operating system for more and return that. 6. Otherwise: A. Is the current space available (check is_available from the mem_control_block)? B. If it is: i) Is it large enough (check "size" from the mem_control_block)? ii) If so: a. Mark it as unavailable b. Move past mem_control_block and return the pointer iii) Otherwise: a. Move forward "size" bytes b. Go back go step 4 C. Otherwise: i) Move forward "size" bytes ii) Go back to step 4
我们基本上是使用寻找开放块的链接指针遍历内存。这是代码:
主分配器
void *malloc(long numbytes) { /* Holds where we are looking in memory */ void *current_location; /* This is the same as current_location, but cast to a * memory_control_block */ struct mem_control_block *current_location_mcb; /* This is the memory location we will return. It will * be set to 0 until we find something suitable */ void *memory_location; /* Initialize if we haven't already done so */ if(! has_initialized) { malloc_init(); } /* The memory we search for has to include the memory * control block, but the users of malloc don't need * to know this, so we'll just add it in for them. */ numbytes = numbytes + sizeof(struct mem_control_block); /* Set memory_location to 0 until we find a suitable * location */ memory_location = 0; /* Begin searching at the start of managed memory */ current_location = managed_memory_start; /* Keep going until we have searched all allocated space */ while(current_location != last_valid_address) { /* current_location and current_location_mcb point * to the same address. However, current_location_mcb * is of the correct type, so we can use it as a struct. * current_location is a void pointer so we can use it * to calculate addresses. */ current_location_mcb = (struct mem_control_block *)current_location; if(current_location_mcb->is_available) { if(current_location_mcb->size >= numbytes) { /* Woohoo! We've found an open, * appropriately-size location. */ /* It is no longer available */ current_location_mcb->is_available = 0; /* We own it */ memory_location = current_location; /* Leave the loop */ break; } } /* If we made it here, it's because the Current memory * block not suitable; move to the next one */ current_location = current_location + current_location_mcb->size; } /* If we still don't have a valid location, we'll * have to ask the operating system for more memory */ if(! memory_location) { /* Move the program break numbytes further */ sbrk(numbytes); /* The new memory will be where the last valid * address left off */ memory_location = last_valid_address; /* We'll move the last valid address forward * numbytes */ last_valid_address = last_valid_address + numbytes; /* We need to initialize the mem_control_block */ current_location_mcb = memory_location; current_location_mcb->is_available = 0; current_location_mcb->size = numbytes; } /* Now, no matter what (well, except for error conditions), * memory_location has the address of the memory, including * the mem_control_block */ /* Move the pointer past the mem_control_block */ memory_location = memory_location + sizeof(struct mem_control_block); /* Return the pointer */ return memory_location; }
要构建malloc兼容的分配器(实际上,我们缺少一些函数realloc(),但是malloc()并且free()是主要函数),请运行以下命令:
编译分配器
gcc -shared -fpic malloc.c -o malloc.so
这将生成一个名为malloc.so的文件,它是一个包含我们代码的共享库。
在UNIX系统上,您现在可以malloc()通过执行以下操作来使用分配器代替系统:
替换标准malloc
LD_PRELOAD=/path/to/malloc.so export LD_PRELOAD
在LD_PRELOAD环境变量会导致动态链接任何可执行它加载之前加载给定的共享库的符号。它还优先考虑指定库中的符号。因此,我们从现在开始在此会话中使用的任何应用程序将使用我们的malloc()而不是系统。一些应用程序不使用malloc(),但它们是例外。其他人使用其他内存管理功能,例如realloc()对内部行为做出错误假设,malloc()可能会崩溃。
如果您想确保malloc()正在使用它,您应该通过write()在函数的入口点添加调用来测试它。
上面的例子还有很多不足之处,但它有助于展示内存管理器需要做什么。它的一些缺点包括:
由于它在系统中断(全局变量)上运行,因此它不能与任何其他分配器共存或与之共存mmap。
当分配内存,在最坏的情况下,将有跨越走所有进程的内存中; 这可能包括磁盘上的大量内存,这意味着操作系统将不得不花费时间将数据移入和移出磁盘。
对于内存不足错误没有优雅的处理(malloc简单地假定成功)。
它没有实现许多其他的内存函数,例如realloc()。
因为sbrk()可能会返回比我们要求更多的内存,所以会内存泄露
该is_available标志使用一个完整的4字节字,即使它只包含1位信息。
分配器不是线程安全的。
分配器无法将可用空间合并为更大的块。
分配器的简化拟合算法导致许多潜在的内存碎片。
也许还有好多其他问题
有许多实现malloc(),每个都有自己的优点和缺点。设计分配器时有许多权衡决策,包括:
分配速度
解除分配的速度
线程环境中的行为
内存接近填充时的行为
缓存局部性
簿记内存开销
虚拟内存环境中的行为
小型或大型物体
实时保证
每种实现都有其自身的优点和缺点。在我们的简单分配器中,它的分配非常慢,但在释放时非常非常快。此外,由于虚拟内存系统的不良行为,它最适合大型对象。
还有许多其他分配器可用。其中一些包括:
Doug Lea Malloc: Doug Lea Malloc实际上是一个完整的分配器系列,包括Doug Lea的原始分配器,GNU libc分配器和ptmalloc。Doug Lea的分配器具有与我们的版本非常相似的基本结构,但它结合了索引以使搜索更快,并且能够将多个未使用的块组合成一个大块。它还使缓存能够更快地重用最近释放的内存。ptmalloc是Doug Lea Malloc的一个版本,它被扩展为支持多线程。有关Doug Lea的Malloc实现的文章可在本文后面的“ 相关主题”部分中找到。
BSD Malloc: BSD Malloc,与4.2 BSD一起发布的实现,包含在FreeBSD中,是一个分配器,用于从预定大小的对象池中分配对象。它具有对象大小的大小类,它是2的幂减去常量。因此,如果您请求给定大小的对象,它只需分配适合该对象的任何大小类。这提供了快速实现,但可能浪费内存。有关此实现的文章可在“ 相关主题”部分中找到。
Hoard: Hoard的编写目标是在多线程环境中非常快。因此,它围绕充分利用锁定来保持任何进程不必等待分配内存。它可以大大加速多线程进程,这些进程可以进行大量的分配和解除分配。有关此实现的文章可在“ 相关主题”部分中找到。
这些是众多可用分配器中最着名的。如果您的程序有特定的分配需求,您可能更喜欢编写一个与程序分配内存的方式相匹配的自定义分配器。但是,如果您不熟悉分配器设计,则自定义分配器通常会产生比它们解决的问题更多的问题。有关该主题的详细介绍,请参阅Donald Knuth的“计算机编程艺术第1卷:基本算法 ”第2.5节“动态存储分配”(参见参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,但大多数算法都基于那里提供的算法。
在C ++中,您可以通过重载在每个类或每个模板的基础上实现自己的分配器operator new()。Andrei Alexandrescu的Modern C ++ Design在第4章“小对象分配”中描述了一个小对象分配器(参见参考资料中的链接)。
我们的内存管理器不仅存在缺点,malloc()而且无论您使用哪种分配器,都存在许多基于内存的管理缺陷。malloc()对于那些需要长时间保存的长存储程序来说,管理内存非常艰巨。如果你有很多对内存浮动的引用,通常很难知道何时应该释放它。其寿命仅限于当前函数的内存相当容易管理,但对于超出该范围的内存,它变得更加困难。此外,许多API尚不清楚内存管理的责任在于调用程序还是被调用函数。
由于管理内存的问题,许多程序都围绕其内存管理规则。C ++的异常处理使这项任务更加成问题。有时似乎更多的代码专门用于管理内存分配和清理,而不是实际完成计算任务!因此,我们将研究内存管理的其他替代方案。
5.1参考计数
引用计数是一种半自动内存管理技术,这意味着它需要一些程序员支持,但它不需要您确定何时不再使用对象。引用计数机制为您做到了这一点。
在引用计数中,所有共享数据结构都有一个字段,其中包含当前对该结构有效的“引用”数。当过程传递指向数据结构的指针时,它会添加到引用计数。基本上,您告诉数据结构存储了多少个位置。然后,当您的过程使用完毕后,它会减少引用计数。发生这种情况时,它还会检查计数是否已降至零。如果是这样,它会释放内存。
这样做的好处是,您不必遵循程序中可能遵循的给定数据结构的每个路径。对它的每个本地化引用只是适当地增加或减少计数。这可以防止它在仍在使用时被释放。但是,每当使用引用计数数据结构时,必须记住运行引用计数功能。此外,内置函数和第三方库将不知道或无法使用您的引用计数机制。对于具有循环引用的结构,引用计数也存在困难。
要实现引用计数,您只需要两个函数 - 一个用于增加引用计数,另一个用于减少引用计数,并在计数降至零时释放内存。
示例引用计数功能集可能如下所示:
基本引用计数功能
/* Structure Definitions*/ /* Base structure that holds a refcount */ struct refcountedstruct { int refcount; } /* All refcounted structures must mirror struct * refcountedstruct for their first variables */ /* Refcount maintenance functions */ /* Increase reference count */ void REF(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount++; } /* Decrease reference count */ void UNREF(void *data) { struct refcountedstruct *rstruct; rstruct = (struct refcountedstruct *) data; rstruct->refcount--; /* Free the structure if there are no more users */ if(rstruct->refcount == 0) { free(rstruct); } }
REF并且UNREF可能会更复杂,这取决于你想做的事。例如,您可能希望为多线程程序添加锁定,并且您可能希望进行扩展,refcountedstruct以便它还包含一个指向要在释放内存之前调用的函数的指针(就像面向对象语言中的析构函数 - 这是必需的)如果你的结构包含指针)。
使用REF和时UNREF,您需要遵守这些指针分配规则:
UNREF 赋值前左侧指针指向的值。
REF 赋值后左侧指针指向的值。
在传递refcounted结构的函数中,函数需要遵循以下规则:
REF函数开头的每个指针。
UNREF函数末尾的每个指针。
使用引用计数的示例
/* EXAMPLES OF USAGE */ /* Data type to be refcounted */ struct mydata { int refcount; /* same as refcountedstruct */ int datafield1; /* Fields specific to this struct */ int datafield2; /* other declarations would go here as appropriate */ }; /* Use the functions in code */ void dosomething(struct mydata *data) { REF(data); /* Process data */ /* when we are through */ UNREF(data); } struct mydata *globalvar1; /* Note that in this one, we don't decrease the * refcount since we are maintaining the reference * past the end of the function call through the * global variable */ void storesomething(struct mydata *data) { REF(data); /* passed as a parameter */ globalvar1 = data; REF(data); /* ref because of Assignment */ UNREF(data); /* Function finished */ }
由于引用计数非常简单,大多数程序员自己实现它而不是使用库。但是,它们依赖于低级分配器,malloc并且free实际上分配和释放它们的内存。
在像Perl这样的高级语言中使用引用计数来进行内存管理。在这些语言中,引用计数由语言自动处理,因此除了编写扩展模块之外,您根本不必担心它。这会带走一些速度,因为一切都必须被引用计数,但增加了相当多的安全性和编程的简易性。以下是好处:
它有一个简单的实现。
它很容易使用。
由于引用是数据结构的一部分,因此它具有良好的缓存局部性。
但是,它也有它的缺点:
它要求您永远不要忘记调用引用计数功能。
它不会释放作为循环数据结构一部分的结构。
它几乎减慢了每个指针的速度。
使用引用计数对象时,在使用异常处理(例如try或setjmp()/ longjmp())时必须采取其他预防措施。
它需要额外的内存来处理引用。
引用计数器占据结构中的第一个位置,这是大多数机器上访问速度最快的位置。
在多线程环境中执行起来更慢,更困难。
C ++可以通过使用智能指针来缓解一些程序员错误,智能指针可以处理指针处理细节,例如引用计数。但是,如果您必须使用任何无法处理智能指针的遗留代码(例如与C库的链接),它通常会退化成一个实际上比不使用它们更加困难和扭曲的混乱。因此,它通常仅对C ++有用 - 仅限项目。如果你想使用智能指针,你真的需要阅读Alexandrescu的Modern C ++ Design书中的“Smart Pointers”一章。
5.2内存池
内存池是半自动化内存管理的另一种方法。内存池有助于为经历特定阶段的程序自动执行内存管理,每个阶段都具有仅为特定处理阶段分配的内存。例如,许多网络服务器进程分配了大量的每个连接内存 - 内存的最大生命周期是当前连接的生命周期。Apache使用池化内存,它的连接分为几个阶段,每个阶段都有自己的内存池。在阶段结束时,立即释放整个内存池。
在池化内存管理中,每个分配都指定一个应从中分配的内存池。每个游泳池都有不同的寿命。在Apache中,有一个持续服务器生命周期的池,一个持续连接生命周期的池,一个持续请求生命周期的池,以及其他池。因此,如果我有一系列不会生成持续时间超过连接的数据的函数,我可以直接从连接池中分配它,知道在连接结束时,它将自动释放。另外,一些实现允许注册清理功能,在清除内存池之前调用它,在清除内存之前执行需要执行的任何其他任务(类似于析构函数,对于面向对象的人)。
要在您自己的程序中使用池,您可以使用GNU libc的obstack实现或Apache的Apache Portable Runtime。GNU obstacks很不错,因为它们默认包含在基于GNU的Linux发行版中。Apache Portable Runtime很不错,因为它有许多其他实用程序来处理编写多平台服务器软件的所有方面。要了解有关GNU obstacks和Apache的池化内存实现的更多信息,请参阅相关主题部分中指向其文档的链接。
obstacks的示例代码
#include <obstack.h> #include <stdlib.h> /* Example code listing for using obstacks */ /* Used for obstack macros (xmalloc is a malloc function that exits if memory is exhausted */ #define obstack_chunk_alloc xmalloc #define obstack_chunk_free free /* Pools */ /* Only permanent allocations should go in this pool */ struct obstack *global_pool; /* This pool is for per-connection data */ struct obstack *connection_pool; /* This pool is for per-request data */ struct obstack *request_pool; void allocation_failed() { exit(1); } int main() { /* Initialize Pools */ global_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(global_pool); connection_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(connection_pool); request_pool = (struct obstack *) xmalloc (sizeof (struct obstack)); obstack_init(request_pool); /* Set the error handling function */ obstack_alloc_failed_handler = &allocation_failed; /* Server main loop */ while(1) { wait_for_connection(); /* We are in a connection */ while(more_requests_available()) { /* Handle request */ handle_request(); /* Free all of the memory allocated * in the request pool */ obstack_free(request_pool, NULL); } /* We're finished with the connection, time * to free that pool */ obstack_free(connection_pool, NULL); } } int handle_request() { /* Be sure that all object allocations are allocated * from the request pool */ int bytes_i_need = 400; void *data1 = obstack_alloc(request_pool, bytes_i_need); /* Do stuff to process the request */ /* return */ return 0; }
基本上,在每个主要操作阶段之后,释放该阶段的障碍。但请注意,如果某个过程需要分配比当前阶段更长的内存,那么它也可以使用较长期的障碍,例如连接或全局障碍。该NULL传递给obstack_free()表明,它应该释放obstack的全部内容。其他值可用,但它们通常不那么有用。
使用池内存分配的好处包括:
管理应用程序的内存很简单。
内存分配和释放更快,因为它一次完成一个池。分配可以在O(1)时间内完成,并且池释放是接近的(它实际上是O(n)时间,但除以在大多数情况下使其为O(1)的巨大因子)。
可以预先分配错误处理池,以便在常规内存耗尽时程序仍可以恢复。
有一些非常容易使用的标准实现。
汇集内存的缺点是:
内存池仅适用于分阶段运行的程序。
内存池通常不适用于第三方库。
如果程序结构发生变化,则可能必须修改池,这可能导致重新设计存储器管理系统。
您必须记住需要分配的池。另外,如果你弄错了,可能很难抓住。
垃圾收集
垃圾收集是对不再使用的数据对象的全自动检测和删除。当可用内存低于特定阈值时,通常会运行垃圾收集器。通常,它们从已知可用于程序的“基础”数据集开始 - 堆栈数据,全局变量和寄存器。然后,他们尝试追踪通过这些数据链接的每一条数据。收集器发现的一切都是好的数据; 它找不到的一切都是垃圾,可以被销毁和重复使用。为了有效地管理内存,许多类型的垃圾收集器需要知道数据结构内指针的布局,因此必须是语言本身的一部分才能正常运行
这是一个权衡的世界:性能,易用性,易于实现和线程功能,仅举几例。您可以使用多种内存管理模式来满足您的项目要求。每种模式都有各种各样的实现,每种模式都有其优点和缺点。对于许多项目,使用编程环境的默认技术是很好的,但是当项目有特殊需求时,了解可用选项将有助于您。此表比较了本文中介绍的内存管理策略。
参考资料
GNU C Library手册的obstacks部分提供了obstacks的编程接口。
Apache Portable Runtime文档描述了其池化分配器的接口。
Doug Lea的Malloc是比较流行的内存分配器之一。
ptmalloc源自Doug Lea的malloc,用于GLIBC。
GNU Obstacks(GNU Libc的一部分)是安装最广泛的池化分配器,因为它位于每个基于glibc的系统上。
Apache的池化分配器(在Apache Portable Runtime中)是使用最广泛的池化分配器。
NetBSD也有自己的池化分配器。
Loki C ++库有许多为C ++实现的通用模式,包括智能指针和自定义小对象分配器。
所述Hahns波姆保守垃圾收集器是最流行的开源垃圾收集器,它可以在常规的C / C ++程序一起使用。
由Marshall Kirk McKusick和Michael J. Karels撰写的新的Berkeley UNIX虚拟内存实现讨论了BSD的VM系统。
现代虚拟内存环境中的Malloc由Poul-Henning Kamp讲述了BSD malloc以及它如何与BSD虚拟内存交互。
由Marshall Kirk McKusick和Michael J. Karels 设计的用于4.3BSD UNIX内核的通用内存分配器讨论了内核级分配器。
Doug Lea的Memory Allocator概述了分配器的设计和实现,包括设计选择和权衡。
记忆分配神话和半真相 Hans-Juergen Boehm介绍了垃圾收集的神话。
Hans-Juergen的空间高效保守垃圾收集 Boehm是一篇描述他的C / C ++垃圾收集器的论文。
“内存管理参考”包含大量有关内存管理的参考资料和链接。
C ++中的内存管理讨论了为C ++编写自定义分配器。
编程方案:内存管理讨论了程序员对内存管理的几种选择。
理查德琼斯的垃圾收集参考书目链接到您想要的垃圾收集的任何纸张。
Michael Daconta的 C ++指针和动态内存管理涵盖了许多内存管理技术。
Frantisek Franek在C和C ++中作为编程概念的存储器讨论了开发有效存储器使用的技术和工具,并使存储器相关错误的作用在计算机编程中应该得到应有的重视。
垃圾收集: Richard Jones和Rafael Lins的自动动态内存管理算法描述了最常用的垃圾收集算法。
第2.5节“ 基本算法的动态存储分配” ,Donald Knuth 的“计算机编程艺术”第1卷描述了几种实现基本分配器的技术。
第2.3.5节“ 基本算法中的列表和垃圾收集” ,Donald Knuth 的“计算机编程艺术”第1卷讨论了列表的垃圾收集算法。
第4章“ 现代C ++设计中的小对象分配” 由Andrei Alexandrescu描述了一种高速小对象分配器,它比C ++标准分配器效率更高。
第7章“ 现代C ++设计中的智能指针” 由Andrei Alexandrescu描述了C ++中智能指针的实现。
Jonathan的第8章,“从头开始编程 ”中的“中级内存主题” 包含本文中使用的简单分配器的汇编语言版本。
在Java程序中处理内存泄漏(developerWorks,2001年2月)中,了解导致Java内存泄漏的原因以及何时应该关注它们。
在developerWorks Linux专区中,找到更多适用于Linux开发人员的资源,并扫描我们最受欢迎的文章和教程。
查看developerWorks上的所有Linux技巧和Linux教程。