1. 池化技术
池是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要经常使用的核心资源先申请出来,放到一个池内,由程序自己管理,这样可以提高资源的使用效率,也可以保证本程序占有的资源数量。 经常使用的池技术包括内存池、线程池和连接池(数据库经常使用到)等,其中尤以内存池和线程池使用最多。
2. 内存池概念
内存池(Memory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使用 new、delete、malloc、free 等API申请分配和释放内存,这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
2.1 内存碎片
-
内碎片:
内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;内部碎片是处于区域内部或页面内部的存储块。占有这些区域或页面的进程并不使用这个 存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。(编译器会对数据进行对齐操作,当不是编译器的最小对齐数的整数倍的时候需要添加一些来保证对齐,那么这块为了对齐而添加的就是内碎片) -
外碎片(通常所讲的内存碎片):
假设系统依次分配了16byte、8byte、16byte、4byte,还剩余8byte未分配。这时要分配一个24byte的空间,操作系统回收了一个上面的两个16byte,总的剩余空间有40byte,但是却不能分配出一个连续24byte的空间,这就是外碎片问题。(本来有足够的内存,但是由于碎片化无法申请到稍大一些的连续内存)
3. 实现定长内存池
3.1 定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。使用格式:new (place_address) type或者new (place_address) type(initializer-list),place_address必须是一个指针,initializer-list是类型的初始化列表。
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要**
使用new的定义表达式进行显示调构造函数
**进行初始化。
3.2 完整实现
即实现一个 FreeList,每个 FreeList 用于分配固定大小的内存块,比如用于分配 32字节对象的固定内存分配器,之类的。
优点:
简单粗暴,分配和释放的效率高,解决实际中特定场景下的问题有效。缺点:
功能单一,只能解决定长的内存需求
,另外占着内存没有释放
。
实现的思想:
- 先向内存申请一块大的内存,如果需要,那么就对这块已经申请出来的内存进行切割(减少了和操作系统底层打交道的次数,效率也就提高了,内存池一定是可以解决申请和释放内存的效率的)
- 对于不需要的小块内存,并不是将其进行释放掉,而是使用一个freeList将他们管理起来,如果freeList中有了空余的,那么再次申请内存首先会到自由链表中取,而不是去申请出来的大内存块进行切割
- 对于这个申请出来的小块内存,前4个或者8个字节存放的是下一个小内存块的地址(这是由于在32位平台下指针的大小是4字节,在64位平台下指针则是8字节),这里如何巧妙的进行平台下指针大小的适配,需要好好的进行琢磨。
- (帮助理解3)指针就是地址,那么指针的类型是为了解引用取到大小,对于所申请出来的内存的类型我是不关心的,在32位平台下我就想取出他的前4个字节,然后存放我的下一个小内存的地址,所以把obj强转为int*类型,然后在解引用就可以拿到前4个字节。那如果在64位平台下,就应该取其前8个字节来存放下一个小内存的地址,但是如果都写为取前4个字节的话,这里就会发生指针越界的问题。下述代码所写的Nextobj()接口函数就是为了能够取出小内存中的前4个字节或者8个字节。
我需要的类型是void*,可以自动的适配平台(类比于上述的int类型,就可以相通)
//实现一个定长的内存池(针对某一个具体的对象,所以起名字叫ObjictPool)
#pragma once
#include"Common.h"
template<class T>
class ObjectPool
{
public:
~ObjectPool()
{