内存分配 & C++类对象内存对齐 -------保姆级解释

内存分配方式

内存碎片

​ 所有的内存分配必须起始于可被 4、8 或 16 整除(视 处理器体系结构而定)的地址或者因为MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配给客户。假设当某个客户请求一个 43 字节的内存块时,因为没有适合大小的内存,所以它可能会获得 44字节、48字节等稍大一点的字节,因此由所需大小四舍五入而产生的多余空间就叫内部碎片

外部碎片

频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片。

在这里插入图片描述

连续型分配方式

  • 单一连续分配:

最简单的分配方式,采用覆盖技术。优点是无外部碎片,缺点是只能用于单用户、有内部碎片、存储利用率低。

  • 固定分区分配:

最简单的多道程序存储管理方式,它将用户内存空间划分为若干个固定大小(可以相等,也可以不等,同为4的倍数或其他),每个分区只装入一道作业。当有空闲分区的时候,就从作业队列里选择适当大小的作业装入该分区。为便于内存分配,通常将分区按大小排队,并为之建立一张分区说明表,其中各项包括每个分区的起始地址,大小及状态(是否被分配)

无外部碎片,但是无法实现多进程共享一个主存区。

  • 动态分区分配:

不预先划分内存,在程序装入内存时,根据进程的大小动态地建立分区,并使得分区的大小正好适合进程的需要,因此系统中分区的大小和数目是可变的。

动态分区分配内存方式刚开始是很好地,但是,之后会导致内存出现很多的小的内存块,也就是外部碎片。外部碎片可以通过紧凑来解决,就是操作系统不时地对进程进行移动和整理。但是需要动态重定位寄存器的支持。

动态分区分配在当系统有很大的内存块的时候,分配内存必须要有一个策略。

1)首次适应:地址递增,顺序查找,第一个能满足的即分配给进程。

2)最佳适应:容量递增,找到第一个能满足要求的空闲分区。

3)最坏适应:容量递减,找到第一个能满足要求的分区。

4)邻近适应:循环首次适应算法。

非连续型分配方式

当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:

  1. 检查要访问的虚拟地址是否合法

  2. 查找/分配一个物理页

  3. 填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)

  4. 建立映射关系(虚拟地址到物理地址)

    重新执行发生缺页中断的那条指令
    如果第3步,需要读取磁盘,那么这次缺页中断就是majflt(major fault:大错误),否则就是minflt(minor fault:小错误)。

内存分配的原理

虚拟内存:也叫虚拟地址空间,它是一个逻辑概念,每个进程都有一定的虚拟地址空间,而且每个进程间的地址空间相互独立。从进程的角度来说,每个进程均认为自己独享整个内存空间(一定)。

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brkmmap(不考虑共享内存)。

  1. brk是将数据段(.data)的最高地址指针_edata往高地址推;

  2. mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。(它相当于是一种内存映射的方法)

    这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断(发生上面的非连续分配方式),操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。(在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brkmmapmunmap这些系统调用实现的。)

原理:

情况一、malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系,如图:

在这里插入图片描述

  1. 进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。

  2. 其中,mmap内存映射文件是在堆和栈的中间(例如libc-2.2.93.so,其它数据文件等),为了简单起见,省略了内存映射文件。**_edata指针(glibc里面定义:glibc内存分配就是一个数组加链表的free list)指向数据段的最高地址。 **

  3. 进程调用A=malloc(30K)以后,内存空间如图2:malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。
    只要把_edata+30K就完成内存分配了?
    事实是这样的,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页。**也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。 **

  4. 进程调用B=malloc(40K)以后,内存空间如图3。

情况二、malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:

在这里插入图片描述

  1. 进程调用C=malloc(200K)以后,内存空间如图4: 默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。 这是因为 brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放。
  2. 进程调用D=malloc(100K)以后,内存空间如图5;
  3. 进程调用free©以后,C对应的虚拟内存和物理内存一起释放。

在这里插入图片描述

  1. 进程调用free(B)以后,如图7所示:B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。
  2. 进程调用free(D)以后,如图8所示: B和D连接起来,变成一块140K的空闲内存。
  3. 默认情况下:**当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。**在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩,变成图9所示。

c++中类对象的内存对齐

  • 非静态成员变量总合。(not static)
  • 加上编译器为了CPU计算,作出的数据对齐处理。(c语言中面试中经常会碰到内存对齐的问题)
  • 加上为了支持虚函数(virtual function),产生的额外负担。
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
class Car1{
};

void fun1(void)
{
    int size =0;
    Car1 objCar;
    size = sizeof(objCar);
    printf("%s is %d\n","Class Car1 Size",size);
}

class Car2{
private:
    int nLength;
    int nWidth;
};
void fun2(void)
{
    int size = 0;
    Car2 objCar;
    size = sizeof(objCar);
    printf("%s is %d\n","Class Car2 Size",size);
}

class Car3{
private:
    int nLength;
    int nWidth;
    static int sHight;
};
void fun3(void)
{
    int size =0;
    Car3 objCar;
    size =sizeof(objCar);
    printf("%s is %d\n","Class Car3 Size",size);
}

class Car4{
private:
    char chLogo;
    int nLength;
    int nWidth;
    static int sHigh;
};
void fun4(void)
{
    int size =0;
    Car4 objCar;
    size = sizeof(objCar);
    printf("%s is %d\n","Class Car4 Size",size);
}

class Car5{
public:
    Car5(){};
    ~Car5(){};
public:
    void Fun(){};
};

void fun5(void)
{
    int size =0 ;
    Car5 objCar;
    size = sizeof(objCar);
    printf("%s is %d\n","Class Car5 Size",size);
}

class Car6{
public:
    Car6(){};
    ~Car6(){};
public:
    void Fun(){};
private:
    int nLength;
    int nWidth;
};
void fun6(void)
{
    int size = 0;
    Car6 objCar;
    size = sizeof(objCar);
    printf("%s is %d\n","Class Car6 Size",size);
}

class Car7{
public:
    Car7(){};
    virtual ~Car7(){};
public:
    void Fun(){};
};
void fun7(void)
{
    int size = 0;
    Car7 objCar;
    size = sizeof(objCar);
    printf("%s is %d\n","Class Car7 Size",size);
}

class Car8{
public:
    Car8(){};
    virtual ~Car8(){};
public:
    void Fun(){};
    virtual void Fun1(){}
};
void fun8(void)
{
    int size = 0;
    Car8 objCar;
    size = sizeof(objCar);
    printf("%s is %d\n","Class Car8 Size",size);
}

int main()
{
    fun1();
    fun2();
    fun3();
    fun4();
    fun5();
    fun6();
    fun7();
    fun8();
}
/*
输出结果
Class Car1 Size is 1
Class Car2 Size is 8
Class Car3 Size is 8
Class Car4 Size is 12
Class Car5 Size is 1
Class Car6 Size is 8
Class Car7 Size is 8
Class Car8 Size is 8
*/
  1. 空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同)
  2. 一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
  3. 因此一个对象的大小≥所有非静态成员大小的总和;
  4. 当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable
  5. 虚继承的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr(virtual function table)指针指向虚函数表vfTable和一个vbPtr(virtual base pointer)指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);
    虚继承表中存放的是该对象地址相对存放vptr指针的偏移量;基类对象部分相对于存放vbptr指针的地址的偏移量;存在虚继承关系的对象才存在虚继承表
  6. 在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;

类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和 + vfptr指针(多继承下可能不止一个) +vbptr指针(多继承下可能不止一个) + 编译器额外增加的字节。

	class Base {
	public:
		Base(int a, int b) :a1(a), a2(b) {};
		virtual void fun1() {};
	private:
		int a1;
		int a2;
	};
	class Inherit :public virtual Base {
	public:
		Inherit(int a, int b, int a3) :Base(a,b), c(a3) {};
		virtual void fun2() {};
	private:
		int c;
	};

void test (){
    Inherit t(1,2,3);
    int *pc = (int*)&t;
		cout << pc << "	" << pc[0] << endl;		//Inherit 的虚函数表
		cout << pc + 1 << "	" << pc[1] << endl;	//Inherit 的虚基类表(存在虚继承)
		cout << pc + 2 << "	" << pc[2] << endl ;//Inherit 的数据成员(非静态)
		cout << pc + 2 << "	" << pc[3] << endl ;//Base的虚函数表
		cout << pc + 2 << "	" << pc[4] << endl ;//Base的数据成员(非静态)
		cout << pc + 2 << "	" << pc[5] << endl ;
}

数据内存对齐(非静态成员变量)

  • 第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

  • 在数据成员完成各自对齐之后,类(结构或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

    很明显#pragma pack(n)作为一个预编译指令用来设置多少个字节对齐的。值得注意的是,n的缺省数值是按照编译器自身设置,一般为8,合法的数值分别是1、2、4、8、16。(即编译器只会按照1、2、4、8、16的方式分割内存。若n为其他值,是无效的。)

在这里插入图片描述

内存分配过程:(对齐当前数据的内存最大值)

  1. char和编译器默认的内存缺省分割大小比较,char比较小,分配一个字节给它。
  2. int和编译器默认的内存缺省分割大小比较,int比较小,占4字节。只能空3个字节,重新分配4个字节。
  3. short和编译器默认的内存缺省分割大小比较,short比较小,占2个字节,分配2个字节给它。
  4. 对齐结束类本身也要对齐,所以最后空余的2个字节也被test占用。

在这里插入图片描述

  1. int和编译器默认的内存缺省分割大小比较,int比较小,占4字节。分配4个字节给int。
  2. char和编译器默认的内存缺省分割大小比较,char比较小,分配一个字节给它。
  3. short和编译器默认的内存缺省分割大小比较,short比较小,此时前面的char分配完毕还余下3个字节,足够short的2个字节存储,所以short紧挨着。分配2个字节给short。
  4. 对齐结束类本身也要对齐,所以最后空余的1个字节也被该对象占用。

可以使用#pragma pack(n)来决定字节对齐的标准。

STL对小内存快请求与释放的处理

STL考虑到小型内存区块的碎片问题,设计了双层级配置器,第一级配置直接使用malloc()和free();第二级配置器则视情况采用不同的策略,当配置区大于128bytes时,直接调用第一级配置器;当配置区块小于128bytes时,便不借助第一级配置器,而使用一个memory pool来实现。由一个宏定义来控制使用第一级配置器还是第二级配置器。SGI STL中默认使用第二级配置器。
二级配置器会将任何小额 区块的内存需求量上调至8的倍数,(例如需求是30bytes,则自动调整为32bytes),并且在它内部会维护16个free-list(数组加链表结构), 各自管理大小分别为8, 16, 24,…,128bytes的小额区块,这样当有小额内存配置需求时,直接从对应的free list中拔出对应大小的内存(8的倍数);当客户端归还内存时,将根据归还内存块的大小,将需要归还的内存插入到对应free list的最顶端。

STL中的内存分配器实际上是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。

  1. 小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。
  2. 避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。
  3. 尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域,
    但是,这种内存分配器局限于STL容器中使用,并不适合一个通用的内存分配。因为它要求在释放一个内存块时,必须提供这个内存块的大小,以便确定回收到哪个free list中,而STL容器是知道它所需分配的对象大小的,比如上述:
    std::vector array;
    array是知道它需要分配的对象大小为array.size()。一个通用的内存分配器是不需要知道待释放内存的大小的,类似于free§。
  4. 还可以减少每个数据申请时所消耗的cookie、Debugger Header 、Pad资源。

本文作者:"( ̄y▽, ̄)╭ "
本文链接:https://blog.csdn.net/weixin_42703404/article/details/106358090
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!


  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值