堆区的动态内存分配

【前言】前面有一篇文章介绍了堆区栈区的区别。栈区的核心主要集中在操作一个栈结构,一般由操作系统维护。堆区,主要是我们程序员来维护,核心就是动态内存分配。

一、动态内存分配器

    虽然低级的mmap和munmap函数来创建和删除虚拟内存区域,但是C程序运行时在需要额外的存储空间时,一般会使用动态存储器分配器,它维护着一个进程的虚拟存储器区域,称为堆。堆是一个请求二进制零的区域,内核为每个进程维护一个变量 brk ,指向堆的顶部。分配器将堆视为一组不同大小的块,每个块为虚拟存储器的一个连续组块,是已分配的或空闲的。

  有两种分配器。显式分配器要求应用显式地释放任何已分配的块,如C的 malloc/free 和C++的 new/delete 。隐式分配器则由分配器检测不再被使用的已分配块,并释放块,也称为垃圾收集器,Lisp、ML和Java等高级语言使用垃圾收集。

二、显式分配器

C标准库提供了 malloc 程序包作为显式分配器,包括 malloc 、 calloc 、 realloc 、 free 函数。malloc返回一个指针,会自动数据对齐。32系统分配的块的地址总是8的倍数,64位系统是16的倍数。malloc不初始化他返回的内存,calloc将内存初始化为0,realloc改变一个以前分配的大小。

动态存储分配器可以使用 mmap 和 munmap 函数显式地分配和释放堆,还有 sbrk 函数:

#include <unistd.h>

/** 将内核的brk指针增加increment来扩展和收缩堆,increment为0时返回brk当前值
 * @return      返回brk的旧值,出错返回-1,并设errno为ENOMEM */
void *sbrk(intptr_t increment);

显式分配器有一些约束条件:

  • 能够处理任意(分配和释放)请求的序列,释放请求必须对应以前分配请求分配的块。
  • 立即响应请求。
  • 对齐块,使可以保存任何类型的数据对象,因此大多数系统中分配器返回的块为8字节对齐的。
  • 不修改已分配的块。

分配器力图做到吞吐量最大化和存储器利用率最大化,在两者之间平衡。吞吐量指单位时间内完成的请求数,一般要求分配请求的最差运行时间和空闲块的数量成线性关系,释放请求的运行时间为常数。描述存储器利用率常用峰值利用率,即请求序列的某个时刻时已分配的总有效载荷和堆的当前大小(为整个请求序列时间的最大值)的比值。

碎片会造成堆的利用率低,产生于未使用的存储器不能满足分配请求的情况。有内部碎片和外部碎片。内部碎片在已分配块比有效载荷大时发生,比如由于对齐要求。外部碎片在没有单独的空闲块足够满足请求时发生,尽管它们合起来足够大。

分配器需要处理空闲块的组织,放置、分隔和合并块。实际的分配器会使用一些数据结构来区别块边界,已分配块和空闲块。

三、隐式空闲链表

下图中示意了用隐式空闲链表来组织堆的方式。

简单的堆块的格式和隐式空闲链表的组织

1、放置块时,分配器搜索空闲链表,常见有首次适配、下一次适配和最佳适配的放置策略。首次适配从头开始搜索空闲链表,下一次适配从链表的上一次查询结束的地方开始搜索,最佳适配检查所有空闲块,选择最小满足的。下一次适配运行最快,但利用率低得多;最佳适配最慢,利用率最高。

2、分配器找到匹配的空闲块后,根据情况可能分割它。如果没有合适的空闲块,合并空闲块来创建更大的空闲块。如果还是不能满足需要,分配器向内核请求额外的堆存储器,转成空闲块加入到空闲链表中。

3、分配器可以选择立即合并或推迟合并,一般为防止抖动,会采用某种形式的推迟合并。

4、合并需要在常数时间内完成,对于空闲链表来说,它是单链表,可以方便地查看后面的块是否空闲块,但前面的块则不行,一个好办法是在块的脚部使用边界标记,它是头部的副本,这样就可以在常数时间查看前后块的类型了。为了避免边界标记占用空间,可以只在空闲块中加边界标记。

四、显式空闲链表

  对于通用的分配器,隐式空闲链表并不适合,因为它的块分配和堆块的总数呈线性关系。可以在空闲块中增加一种显式的数据结构。下面是双向空闲链表的堆块的格式。双向链表使首次适配时间从块总数的线性时间减少到了空闲块数的线性时间。

双向空闲链表的堆块的格式

显式链表的缺点是空闲块必须足够大来包含结构,这增大了最小块的大小,也潜在提高了内部碎片的程度。

五、分离的空闲链表

分离的空闲链表利用分离存储来减少分配时间。分配器维护一个空闲链表数组,每个空闲链表为一个大小类。大小类的定义方式有很多,如2的幂。有简单分离存储和分离适配方法。

简单分离存储的大小类的空闲链表包含大小相等的块,块大小为大小类中最大元素的大小。分配和释放块都是常数时间,不分割,不合并,已分配块不需要头部和脚部,空闲链表只需是单向的,因此最小块为单字大小。缺点是很容易造成内部和外部碎片。

分离适配的分配器维护一个空闲链表的数组,每个链表和一个大小类相关联,包含大小不同的块。分配块时,确定请求的大小类,对适当的空闲链表做首次适配。如果找到合适的块,可以分割它,将剩余的部分插入适当的空闲链表中;如果没找到合适的块,查找更大的大小类的空闲链表。分离适配方法比较常见,如GNU malloc包。这种方法既快、利用率也高。

六、垃圾收集

垃圾收集器是一种动态存储分配器,自动释放程序不再需要的已分配块(垃圾)。支持垃圾收集的系统中,应用显式分配堆块,但从不显式释放它们。

垃圾收集器将存储器视为一个有向可达图,节点分为根节点和堆节点,堆节点对应堆中的已分配块,根节点对应包含指向堆中的指针但不在堆中的位置,如寄存器、栈里的变量、虚拟存储器中读写数据区域内的全局变量。当存在根节点到p的有向路径时,称p是可达的,不可达节点无法被应用再次使用,即为垃圾。

Java等语言对于创建和使用指针有严格的控制,能够回收所有垃圾。C/C++语言的垃圾收集器通常不能维护可达图的精确表示,称为保守的垃圾收集器,它不能回收所有垃圾。

七、和存储器有关的错误

在使用C语言和虚拟存储器打交道时,很容易犯一些错误,而且它们常常是致命的。

  • 间接引用坏指针。间接引用指向空洞或只读区域的指针,会造成段异常或保护异常而终止。
  • 读未初始化的存储器。.bss存储器位置总是被加载器初始化为0,但堆存储器不是这样,假定它为0会造成不可预料的结果。
  • 允许栈缓冲区溢出。不检查串的大小就写入栈中的目标缓冲区可能会有缓冲区溢出错误。
  • 假设指针和指向的对象大小相同。这可能会导致分配器的合并代码失败,但没有明显的原因。
  • 造成错位错误。如超出循环造成覆盖错误。
  • 引用指针,而不是指向的对象。
  • 误解指针运算。指针的算术操作是以指向的对象的大小为单位进行的,而不是字节。
  • 引用不存在的变量。比如栈中的局部变量,栈弹出后它就不再合法了。
  • 引用空闲堆块中的数据。和上一个类似,这回发生在被释放的堆中。
  • 引起存储器泄漏。忘记释放已分配块,产生垃圾,对于不终止的程序(守护进程、服务器),存储器泄漏的错误非常严重。

原文地址:https://www.cnblogs.com/huangfuyuan/p/9190371.html

时间: 2024-10-06 02:11:07

堆区的动态内存分配的相关文章

【好程序员笔记分享】——动态内存分配

<A href="http://www.goodprogrammer.org/" target="blank">ios培训</A>------我的c语言笔记,期待与您交流! 再C语言中关于内存是一个很重要的知识点,所以今天我就从c语言的内存分配开始为大家解析一下C语言再iOS开发中非常重要的一些知识. 1:malloc函数的介绍 C语言中开辟内存空间:malloc函数 再C语言中malloc原理大致是这样的: malloc函数的实质体现在,它

简单理解动态内存分配和静态内存分配的区别

在涉及到内存分配时,我们一般都要考虑到两种内存分配方式,一种是动态内存分配,另一种是静态内存分配,我们该怎么理解这两者的区别呢? 在我看来,静态内存分配和动态内存分配比较典型的例子就是数组和链表,数组的长度是预先定义好的,在整个程序中是固定不变的,所以他在内存分配时是以静态内存分配的方式进行的.而链表,它的信息有可能会随时更改,内存的分配取决于我们实际输入的数据,这样就用到了动态内存分配的方式. 静态内存分配是在程序编译或者运行过程中,按事先规定的大小分配内存空间的分配方式,他的前提的必须事先知

C++动态内存分配

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解>电子工业出版社等. CSDN视频网址:http://edu.csdn.net/lecturer/144 C / C ++中的动态内存分配是指程序员手动执行内存分配, 动态分配的内存分配给堆,非静态和局部变量获取在Stack上分配的内存.详情查看上篇博文:C程序的内存布局. 什么是应用程序? 动态分配的

SQLite剖析之动态内存分配

SQLite通过动态内存分配来获取各种对象(例如数据库连接和SQL预处理语句)所需内存.建立数据库文件的内存Cache.以及保存查询结果.我们做了很多努力来让SQLite的动态内存分配子系统可靠.可预测.健壮并且高效.本文概述SQLite的动态内存分配,软件开发人员在使用SQLite时可以据此获得最佳性能. 1.特性    SQLite内核和它的内存分配子系统提供以下特性:    (1)对内存分配失败的健壮处理.如果一个内存分配请求失败(即malloc()或realloc()返回NULL),SQ

【Cpp】考点·堆栈&amp;动态内存分配

动态内存分配 堆内存分配与释放 C/C++定义了四个内存区间:代码区,全局变量与静态变量区,局部变量区(栈区),动态内存区(堆区) 通常定义变量(或对象),编译器在编译时都可以根据该变量(或对象)的类型知道所需内存空间的大小,从而系统在适当的时候为他们分配确定的存储空间.这种内存分配称为静态存储分配.有些操作对象只在程序运行时才确定,这样编译时无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方法称为动态存储分配.所有动态存储分配都在堆区中进行. 当程序运行到需要一

C++ 动态内存分配(6种情况,好几个例子)

1.堆内存分配 : C/C++定义了4个内存区间: 代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆(heap)区或自由存储区(free store). 堆的概念: 通常定义变量(或对象),编译器在编译时都可以根据该变量(或对象)的类型知道所需内存空间的大小,从而系统在适当的时候为他们分配确定的存储空间.这种内存分配称为静态存储分配: 有些操作对象只在程序运行时才能确定,这样编译时就无法为他们预定存储空间,只能在程序运行时,系统根据运行时的要求进行内存分配,这种方法称为动态存储分

28._动态内存分配

动态内存分配 传统数组的缺点(静态内存分配):   1.数组长度必须事先指定,且只能是常整数,不能是变量    例子:     int a[5]; //OK     int len = 5; int a[len]; //error   2.传统形式定义的数组,该数组的内存程序员无法手动编    程释放,在一个函数运行期间,系统为该函数中数组所     分配的空间会一直存在,直到该函数运行完毕时,数组    元素所占存储空间才会被系统释放 3.数组的长度一旦定义,长度就能被改变     4.A函数

Lesson(DynamicMerry)动态内存分配

//  main.m //  1-27随堂笔记 //讲师: 小辉 //笔者: 王学文 //  Created by lanouhn on 15/1/27. //  Copyright (c) 2015年 lanouhn. All rights reserved. //动态内存分配(DynamicMerry) #import <Foundation/Foundation.h> void test() { int x = 10; int y = 20; } //函数返回栈区的数据,是不安全的;一i

Java静态内存与动态内存分配的解析

1. 静态内存 静态内存是指在程序开始运行时由编译器分配的内存,它的分配是在程序开始编译时完成的,不占用CPU资源. 程序中的各种变量,在编译时系统已经为其分配了所需的内存空间,当该变量在作用域内使用完毕时,系统会 自动释放所占用的内存空间. 变量的分配与释放,都无须程序员自行考虑. eg:基本类型,数组 2. 动态内存 用户无法确定空间大小,或者空间太大,栈上无法分配时,会采用动态内存分配. 3. 区别 a) 静态内存分配在编译时完成,不占用CPU资源; 动态内存分配在运行时,分配与释放都占用