目录
内存泄漏
内存模型 、进程地址空间
堆和栈的区别
内存对齐
大端小端及判断
虚拟内存有什么作用
内存泄漏
概念:
是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况, 内存泄漏并不是指内存在物理上的消失, 而是应用程序分配了某段内存后, 因为设计错误, 失去了对该段内存的控制, 因而造成了内存的浪费.
- new和malloc申请资源使用后, 没有用delete和free释放
- 子类继承父类时, 父类的析构函数不是虚函数
- 未关闭的文件或资源
危害:
长期运行的程序出现内存泄漏, 影响很大; 出现内存泄漏会导致响应越来越慢, 最终卡死.
避免:
- 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
- 一定要将基类的析构函数声明为虚函数
- 对象数组的释放一定要用delete []
- 有new就有delete,有malloc就有free,保证它们一定成对出现
- 出问题了使用内存泄漏工具检测。
内存泄漏非常常见,解决方案分为两种:
- 事前预防型。如智能指针等。
- 事后查错型。如泄漏检测工具。
- Linux下可以使用Valgrind工具
- Windows下可以使用CRT库
内存模型 、进程地址空间
如上图,从低地址到高地址,用户空间内存,从低到高分别是 6 种不同的内存段:
- 代码段,包括二进制可执行代码。只读,包含一些只读的变量。
- 数据段,包括已初始化的静态常量和全局变量;
- BSS 段,包括未初始化的静态变量和全局变量;
- 堆段,包括动态分配的内存,从低地址开始向高地址增长;动态申请内存用,由new分配的内存块,其释放由程序员控制(一个new对应一个delete)
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长;最后还有一个共享区,位于堆和栈之间。
- 栈段,存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。在不需要时自动清除的存储区。
Linux下的进程地址空间具体是由mm_struct实现的
struct mm_struct{unsigned int code_start;unsigned int code_end;unsigned int init_start;unsigned int init_end;unsigned int uninit_start;unsigned int uninit_end;unsigned int heap_start;unsigned int heap_end;unsigned int stack_start;unsigned int stack_end;
};
比如堆向上增长,栈向下增长,实际上就是改变mm_struct中的堆和栈的起始指针来实现的!
为什么要有进程地址空间?
为了实现多任务操作系统中的多进程隔离和独立运行,以确保不同进程之间的互不干扰和安全性。
- 隔离和保护:每个进程都有自己独立的地址空间,使得不同进程之间的内存互不干扰。这种隔离性确保了一个进程的错误或恶意行为不会对其他进程造成影响,提高了系统的稳定性和安全性。
- 相对地址一致性:每个进程都认为自己的地址空间是从0开始的,并且认为看到的是相同的地址空间范围。这种相对地址一致性使得进程可以使用相对地址进行内存操作,而不必关心其他进程的地址空间。
- 独立内存:每个进程都认为自己独占内存,可以自由分配和管理自己的内存资源。这使得进程可以在不互相干扰的情况下运行,并且不需要担心其他进程的内存使用情况。
- 虚拟内存:进程地址空间还支持虚拟内存的概念,允许操作系统在物理内存有限的情况下为每个进程提供大于物理内存的虚拟内存空间。这通过将部分数据存储在磁盘上,根据需要进行页面调度,提高了内存利用率。
堆和栈的区别
分配方式:
栈由编译器自动分配和管理,程序员无需手动控制栈内存的分配和释放。局部变量、函数参数以及函数调用上下文等都存储在栈上。
堆由程序员手动申请和释放,通常使用new(C++)或malloc(C)等函数分配内存,并使用delete(C++)或free(C)来释放内存。堆用于存储动态分配的数据,如动态数组、对象实例等。
空间大小限制:
栈的大小通常是有限的,具体大小由编译器或操作系统设置。栈的大小在编译时或运行时可以进行配置,但总是有限的。
堆的大小受限于系统可用的虚拟内存大小,通常比栈要大得多。堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大
栈空间和堆区的大小是由操作系统和编译器决定的,不同系统和编译器可能会有不同的默认值。一般来说,栈空间默认是1MB或2MB,而堆区一般是1GB到4GB之间。
内存管理机制:
栈的内存管理由编译器自动完成,变量的生命周期与其作用域相对应。栈内存的分配和释放是隐式的,不需要程序员干预。
堆的内存管理由程序员手动控制。程序员负责显式地分配堆内存,并在不再需要时释放它。如果不正确地管理堆内存,可能会导致内存泄漏或悬挂指针等问题。
碎片问题:
栈内存通常不会出现碎片问题,因为栈的内存分配和释放都是线性的,按照函数调用的顺序进行。
堆内存可能会出现碎片问题,特别是在频繁进行动态内存分配和释放操作时。这可能导致内存空间的不连续,影响程序的性能。
生长方向:
栈的生长方向通常是向下的,即从高地址向低地址增长。这意味着栈的顶部在分配时逐渐向较低的地址移动。
堆的生长方向通常是向上的,即从低地址向高地址增长。堆内存在动态分配时逐渐向较高的地址分配。
分配效率:
栈由编译器和操作系统提供的支持,分配和释放内存的效率较高,通常采用硬件级别的指令进行操作。
堆的内存分配和释放需要程序员显式地调用函数,效率相对较低,并可能涉及复杂的内存管理机制。
形象的比喻
栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
内存对齐
内存对齐涉及到如何存储和访问数据以提高计算机的性能和效率。确保数据按照一定的规则存储在内存中,以便于有效地访问。
结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。(即结构体的首地址处,即对齐到0处)
- 其他成员变量要对齐到最小对齐数的整数倍的地址处。
- 结构体的总大小为最大对齐数的整数倍。
对齐数 = 该结构体成员变量自身的大小与编译器默认的一个对齐数的较小值。
注:VS中的默认对齐数为8,不是所有编译器都有默认对齐数,当编译器没有默认对齐数的时候,成员变量的大小就是该成员的对齐数。
要修改编译器的默认对齐数,我们需要借助于以下预处理命令:#pragma pack(...)
为什么存在内存对齐?
平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。
- 比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。
性能原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次。
其实结构体的内存对齐是拿空间来换取时间的做法。
大端小端及判断
大端模式:是指数据的低位(就是权值较小的后面那几位)保存在内存的高地址中,而数据的高位,保存在内存的低地址中;地址由小向大增加,而数据从高位往低位放。
小端模式:是指数据的高位(就是权值较大的前面那几位)保存在内存的高地址中,而数据的低位,保存在内存的低地址中;地址由大向小增加,而数据从低位往高位放 。
大小端的意义在于确保数据在不同的计算机体系结构之间正确传递、解释和处理,保证系统之间的互操作性和数据的可移植性。
例如:32bit的数字0x12345678
所以在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输
小端模式中的存储方式为:
大端模式中的存储方式为:
如何在代码中进行判断呢?
方式一:使用强制类型转换-这种法子不错
#include <iostream>
using namespace std;
int main()
{int a = 0x1234;//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分char c = (char)(a);if (c == 0x12)cout << "big endian" << endl;else if(c == 0x34)cout << "little endian" << endl;
}
方式二:巧用union联合体
#include <iostream>
using namespace std;
// union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{int a;char ch;
};
int main()
{endian value;value.a = 0x1234;//a和ch共用4字节的内存空间if (value.ch == 0x12)cout << "big endian"<<endl;else if (value.ch == 0x34)cout << "little endian"<<endl;
}
虚拟内存有什么作用
- 虚拟内存可以使得进程对运行内存超过物理内存大小
- 因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。当进程需要访问被置换出去的页时,它们会被重新加载到物理内存中。这种机制允许了更大的程序运行,而不受物理内存的限制。
- 提高内存利用率:
- 虚拟内存系统可以更好地利用物理内存资源。只有进程当前需要的部分内存被加载到物理内存中,而不是将整个程序加载到内存中。这减少了内存浪费,允许多个进程在有限的物理内存中共存。
- 虚拟内存为每个进程提供了独立的地址空间
- 每个进程有自己的页表,这样,一个进程无法直接访问其他进程的内存,从而提高了安全性和隔离性。即使两个进程使用相同的虚拟地址,它们映射到不同的物理内存位置。
- 页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。