首先提出一道经典的面试题来引出今天的主角:
进程的虚拟空间分布是什么样的,全局变量放在哪里?
在数据初始化之后,全局变量放在.data
段
在数据未初始化时,全局变量放在.bss
段
内存五区
进程虚拟内存主要分为五个部分:
- 栈区(自上而下)
- 堆区(自下而上)
- 全局区也叫做静态区(也就是用户栈)
- 常量区
- 程序代码区
在内核虚拟内存中物理内存、内核代码和数据对于每一个进程都是一样的。
唯一不同的是进程相关的数据结构。
栈区
栈区是自上而下的扩展的,是一块连续的内存空间。栈和堆总共的空间大小在一开始就被规定好了。他们都会随着程序的进行而发生变化。
栈是由系统自动分配的,速度很快,并且释放的也很快,程序员无法主动去控制栈的释放。如果在栈申请时没有剩余的空间就会出现OOM
的情况。
栈主要用来存放局部变量和函数参数。比如你的递归函数,函数内部声明的变量等。
堆区
堆区是自下而上的扩展的,是一块不连续的内存空间。这一点和栈完全相反。
堆在程序启动初期会根据程序分配内存空间,并且在程序运行期间仍会继续分配。程序员是可以主动去申请堆内存的,分配方式类似于链表,申请的时候会以遍历的方式在堆中找到一块合适的内存块。然后将该内存块从空闲结点中删除掉。
存储在堆中的内存是需要程序员去手动释放掉的。如果不释放这些资源,就会导致内存一直被占用,最终变得越来越多。因此许多语言在继C/C++之后采用了垃圾回收机制。垃圾回收机制可以帮助程序员主动的释放掉不会再次使用的申请在堆内存上的变量,它可以让程序员更好的关注于业务,而不是释放资源。
注意:频繁的释放堆内存也会造成碎片化的问题,因此尽量不要频繁的创建和释放堆资源。
全局静态区
全局区主要用来存放全局变量和在编译器就能够确定的static
值。因此这一块在程序编译时就已经被分配好内存空间了。
全局变量和静态变量是放在一块的。初始化的全局变量和静态变量在同一个区域内,未初始化的全局变量和静态变量在相邻的另一个区域内。
优点在于:不用主动去分配,由操作系统自动分配,速度快,因为编译期就能被确定并且不容易出错。
文字常量区
常量在统一运行被创建,常量区的内存是只读的,程序结束后由系统释放。
程序代码区
存放二进制代码的地方。
总结
进程的虚拟空间可以分为五大部分:栈、堆、全局区、常量区、程序代码区。
其中栈和堆的比较重要,他们的主要区别有:
- 栈的地址是自上而下递减的,堆的地址是自下而上递增的。(注意虚拟空间的地址是从下到上依次递增的)
- 栈只能由系统控制分配和释放。而堆可以由程序员调用
malloc/new
API函数分配,并且由程序员主动的去释放资源。 - 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。而堆是由语言进行实现的一个内存区。由语言中的函数进行调用和释放。堆中的内存经常需要进行整理才能获得更大的内存空间。
- 频繁的创建和删除堆资源会产生碎片化问题,因为堆的申请方式跟栈不一样,堆是在内存空间中挑的一块合适的地方。而栈是一段连续的内存。并且栈释放的时候也是挨着释放的,因为出栈的肯定是要从栈顶出。
那么我们上述说了频繁的释放堆资源会造成碎片化的问题,而项目中又经常会用到堆资源,那有没有好的解决办法呢?
答案就是内存池技术?
什么是内存池?
**内存池(Memory Pool)**是一种内存分配方式。通常我们习惯直接使用new、malloc
等API申请分配内存。这样做的缺点在于:所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池诞生原因
默认内存管理函数的不足
利用默认的内存管理函数new/delete
或malloc/free
在堆上分配和释放内存会有一些额外的开销。
系统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。
如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。然后系统更新内存空闲块表,完成一次内存分配。类似地,在释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空闲块合并成较大的空闲块。
默认的内存管理函数还考虑到多线程的应用,需要在每次分配和释放内存时加锁,同样增加了开销。可见,如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率。
默认的分配和释放内存算法自然也考虑了性能,然而这些内存管理算法的通用版本为了应付更复杂、更广泛的情况,需要做更多的额外工作。而对于某一个具体的应用程序来说,适合自身特定的内存分配释放模式的自定义内存池则可以获得更好的性能。
内存池的分类
应用程序自定义的内存池根据不同的适用场景又分为不同的类型。
从线程安全的角度上来看,内存池可以分为单线程内存池和多线程内存池。
-
单线程内存池整个生命周期只被一个线程使用,无需考虑互斥访问的情况。
-
多线程内存池需要被多个线程共享,因此在每次分配和释放内存时都需要加锁。
相对而言,单线程内存池性能更好,多线程内存池适用范围更广。
内存池有什么优点?
内存池会在你实例化对象,分配内存的时候预先给你分配一定数量、大小相等的内存块。当这些内存不够使用的时候,会从内存池中分出一部分新的内存块来使用。如果内存块也不够的话会再申请新的内存。
这样做的显著优点就是提高了内存分配效率。
经典内存池技术
**内存池(Memory Pool)**技术因为其对内存管理有着显著的优点,在各大项目中应用广泛,备受推崇。但是,通用的内存管理机制要考虑很多复杂的具体情况,如多线程安全等,难以对算法做有效的优化,所以,在一些特殊场合,实现特定应用环境的内存池在一定程度上能够提高内存管理的效率。
经典的内存池(Memory Pool)技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大加快内存分配/释放过程。既然针对是特定对象的内存池,所以内存池一般设置为类模板,根据不同的对象来进行实例化。
经典内存池如何实现?
- 先申请一块连续的内存空间,该段内存空间能够容纳一定数量的对象。
- 每个对象连同一个指向下一个对象的指针一起构成一个内存节点(Memory Node)。各个空闲的内存节点通过指针来形成一个链表,链表的每一个内存节点都是一块可供分配的内存空间。
- 某个内存节点一旦分配出去,就将从链表中去除。
- 一旦释放了某个内存节点的空间,又将该节点重新加入自由内存节点链表。
- 如果一个内存块的所有内存节点分配完毕,若程序继续申请新的对象空间,则会再次申请一个内存块来容纳新的对象。新申请的内存块会加入内存块链表中。