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

  这篇笔记结束就不在高新CSAPP的读书笔记了,一个是基本会的,另一个是暂时看不懂的,还有太复杂细节的。可以参考这个博客的一系列文章CSAPP笔记

一、动态内存分配器

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

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

二、显式分配器

  参考:https://blog.csdn.net/wz1226864411/article/details/77934941

  1、简介

  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字节对齐的。
  • 不修改已分配的块。

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

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

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

2、隐式空闲链表

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

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

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

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

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

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

3、显式空闲链表

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

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

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

4、分离的空闲链表

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

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

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

三、垃圾收集

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

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

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

四、和存储器有关的错误

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

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

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

  1. iOS开发——C篇&动态内存分配

    再C语言中关于内存是一个很重要的知识点,所以今天我就从c语言的内存分配开始为大家解析一下C语言再iOS开发中非常重要的一些知识. 1:malloc函数的介绍 C语言中开辟内存空间:malloc函数 再 ...

  2. SQLite剖析之动态内存分配

    SQLite通过动态内存分配来获取各种对象(例如数据库连接和SQL预处理语句)所需内存.建立数据库文件的内存Cache.保存查询结果. 1.特性    SQLite内核和它的内存分配子系统提供以下特性 ...

  3. C++动态内存分配

    C++动态内存分配1.堆内存分配 :C/C++定义了4个内存区间:代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆(heap)区或自由存储区(free store). 堆的概念:通常定 ...

  4. C++学习笔记(十一):void*指针、类型转换和动态内存分配

    void*指针 void关键字表示“空类型”的概念.但是,这里的“空类型”不表示“任意类型”,而是表示不存在的意思,也就是说C/C++不允许你写语句void a,不存在类型为void的东西. void ...

  5. 转: Linux C 动态内存分配 malloc及相关内容 .

    一.malloc()和free()的基本概念以及基本用法: 1.函数原型及说明: void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针 ...

  6. C—动态内存分配之malloc与realloc的区别

    在程序的执行期间分配内存时,内存区域中的这个空间称为堆(heap).还有另一个内存区域,称为堆栈(stack),其中的空间分配给函数的参数和本地变量.在执行完该函数后,存储参数和本地变量的内存空间就会 ...

  7. 【C/C++】动态内存分配和链表

    本文对链表以及C/C++中的动态链表做详细诠释. 什么是链表? 链表是一种重要的数据结构,它最大的优点是可以进行动态的存储分配.链表有单向链表,双向链表,循环链表.对于c,这里我们只讨论单向链表. 我 ...

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

    1.堆内存分配 : C/C++定义了4个内存区间: 代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆(heap)区或自由存储区(free store). 堆的概念: 通常定义变量(或对 ...

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

    1.堆内存分配 : C/C++定义了4个内存区间: 代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆(heap)区或自由存储区(free store). 堆的概念: 通常定义变量(或对 ...

随机推荐

  1. LCA算法——倍增

    概况 LCA(Lowest Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先. 实现过程 预处理:通过dfs遍历,记录每个节点到根节点的距离dis ...

  2. vue-cli3移动端自适应配置 Vant组件库

    module.exports = { presets: [ '@vue/app' ], plugins: [ ['import', { libraryName: 'vant', libraryDire ...

  3. Java创建线程四种方式

    1.继承Thread类 public class MyThread extends Thread { public MyThread() { } public void run() { for(int ...

  4. zjnu1757Excellent (数学公式)

    Description Let us begin with a positive integer N and find the smallest positive integer which does ...

  5. python常用连接字符串

    1.使用占位符% print(('%s%s%s' % ('one','two', 'three'))) 2.'+'号连接 字符串是不可变对象,每次改变会申请一块新的内存,操作符+连接字符串的时候会涉及 ...

  6. C# 之 async / await

    直接看一个例子 private async void button1_Click(object sender, EventArgs e) { var t = Task.Run(() => { T ...

  7. 添加特定软件证书到windows不信任列表

    $target="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" $filePath=$PSScript ...

  8. 【Java】位操作符

    位运算符 java支持的位运算符有7个,分为两类:位逻辑运算和移位运算.位逻辑运算符包括按位取反(~).按位与(&).按位或(|)和按位异或(^)4种,.移位运算符包括左移(<<) ...

  9. Operating System:信号量

    pv原语操作(1)操作系统PV意思:PV操作与信号量的处理相关,P表示通过的意度思,V表示释放的意思.(2)p操作和v操作是不可中断问的程序段,称为原语.如果将信号量看作共享变量,则pv操作为其临界区 ...

  10. eclispe中解决OOM问题

     -Xms256M -Xmx512M -XX:PermSize=256m -XX:MaxPermSize=512m