内存基础知识总结
1. 虚拟内存
- 图示:内存模型
- 参考链接:
- https://zhuanlan.zhihu.com/p/348171413
- https://zhuanlan.zhihu.com/p/82746153
- https://zhuanlan.zhihu.com/p/495709005
1.1 核心概念与机制
- 地址空间:一个进程的地址空间包含该进程所有相关内存(code / stack / heap 等)。从进程视角看,是一块从 0 到高地址的连续逻辑空间。
- 早期方式:用基址寄存器+界限寄存器把每个进程的整块地址空间映射到物理内存
- 存在以下问题:
- 大小难以限定、不够用难扩展;
- 进程内未使用区域浪费物理内存。
- 存在以下问题:
-
虚拟内存:每个进程使用独立的逻辑地址空间,将逻辑地址空间按「页」划分;只有用到的页才占用物理内存或交换区,实现隔离与按需使用。
- 页(Page):物理内存和进程地址空间都被分成大小相等的块,每块为一页,页内地址连续。进程也按页划分。
- MMU(内存管理单元):位于 CPU 内,负责虚拟地址到物理地址的转换(查页表)。
- 页表机制:多级页表、TLB(Translation Lookaside Buffer)加速地址转换。
- Swap:进程中部分页可仅在磁盘交换区中,需要时再换入物理内存。
- COW(Copy-on-Write):fork 后子进程与父进程先共享物理页,写时再复制,减少冗余占用。
- 内存分配器:glibc malloc、tcmalloc、jemalloc 等(实现与性能对比见后文 malloc 小节)。
内存占用度量指标
- RSS(Resident Set Size):进程实际占用的物理内存。
- VSZ(Virtual Size):进程虚拟地址空间大小。
- PSS(Proportional Set Size):按共享比例分摊后的物理内存(含共享库等)。
- USS(Unique Set Size):进程独占的物理内存。
- Swap:换出到磁盘的内存大小及使用情况。
1.2 进程逻辑内存布局
- 内核空间(kernel):高地址端,含命令行参数、环境变量等。
- 用户空间:
- 栈(stack):函数参数、局部变量、调用上下文;高地址向低地址增长,向下增长。
- 堆(heap):动态分配(如 new/delete);低地址向高地址增长,向上增长;堆和栈中间为可扩展的空闲区。
- 数据区(全局区/静态区):
.bss:未初始化的全局变量、静态变量.data:已初始化的全局变量、静态变量.rodata:只读数据(常量、字面量、const、#define 等)
- 代码区 .text:机器码、指令。
1
2
3
4
5
6
7
MyClass obj1(10); // 栈上创建对象
MyClass* obj2 = new MyClass(20); // 堆上创建对象
obj1.setData(30);
int value = obj1.getData();
delete obj2; // 释放堆内存
1.3 malloc
- bk
- mmp

malloc分配规则
- 分配内存 < M_MMAP_THRESHOLD,走__brk,从内存池获取,失败的话走brk系统调用
- 分配内存 >= M_MMAP_THRESHOLD,走__mmap,直接调用mmap系统调用
2. 基础类型大小
- 1字节 = 8位
- 1Byte(字节) = 8Bit
- 1KB (Kilobyte 千字节) = 1024Byte
- 1MB (Megabyte,兆字节,简称“兆”) = 1024KB
- 1GB (Gigabyte,吉字节,又称“千兆”) = 1024MB
- 1TB (Terabyte,太字节,或百万兆字节) = 1024GB
- 1024=2^10。
| 类型 | 大小(字节) | 位数 | 取值范围 |
|---|---|---|---|
| int8_t | 1 | 8 | -128 ~ 127 |
| int16_t / short | 2 | 16 | -32768 ~ 32767 |
| int32_t / int | 4 | 32 | -2^31 ~ 2^31-1 |
| int64_t / long | 8 | 64 | -2^63 ~ 2^63-1 |
| float | 4 | 32 | 单精度 |
| double | 8 | 64 | 双精度 |
| char | 1 | 8 | ASCII字符 0 ~ 255 |
| char16_t | 2 | 16 | UTF-16字符 |
| char32_t | 4 | 32 | UTF-32字符 |
| bool | 1 | 8 | 布尔值 {0, 1} |
- size_t
- 32 位系统中,size_t 是4字节,uint32_t
- 64 位系统中,size_t 是8字节
- double与float的表示:阶码
- 符号位 + 阶码 + 尾数
- double 1 + 11 + 52
- float 1 + 8 + 23
- 大小端存储
- 大端存储:字数据的高字节存储在低地址中
- 小端存储:子数据的低字节存储在低地址中
- sizeof的结果单位是字节
- mutex 内存占用40字节
- shared_ptr 内存占用16字节。是原始指针的两倍
3. STL容器对象大小
3.1 STL容器大小
| 容器类型 | 对象大小(字节) | 内部结构 |
|---|---|---|
| std::vector | 24 | 指针(8) + size(8) + capacity(8) |
| std::deque | 80 | 分段数组,结构复杂 |
| std::list | 24 | 头指针 + 尾指针 + size |
| std::map | 48 | 红黑树 |
| std::set | 48 | 红黑树 |
| std::string | 32 | SSO优化 |
3.2 Vector内存管理详解
3.2.1 Vector的内存布局
std::vector内存布局:
- 栈上对象本身(24字节)
- 指针(8字节):指向堆上的数据数组
- size(8字节):当前元素数量
- capacity(8字节):已分配的容量
- 堆上数据(capacity × 元素大小)
总内存 = sizeof(vector对象) + capacity × sizeof(元素类型)
示例:
1
2
3
4
std::vector<int> vec{1,2,3,4,5,6,7};
栈内存:24字节
堆内存:7 × 4 = 28字节
总计:52字节
3.2.2 Vector扩容策略
当容量不足时,vector会自动扩容:
容量变化:0 → 1 → 2 → 4 → 8 → 16 → 32 …
扩容规则:
- GCC:通常按2倍增长
- MSVC:通常按1.5倍增长
- 每次扩容都需要重新分配内存并拷贝数据
3.3 String的SSO优化
SSO(Small String Optimization)小字符串优化
string内部结构(32字节):
- 指针(8字节):指向字符串数据
- 长度(8字节):字符串长度
- 本地缓冲区(16字节):用于存储短字符串
| 特性 | 短字符串(SSO) | 长字符串(堆分配) |
|---|---|---|
| 构造速度 | 快(约10ns) | 慢(约100ns) |
| 拷贝速度 | 快(约15ns) | 慢(约100ns) |
| 析构速度 | 快(约1ns) | 慢(约50ns) |
| 内存分配 | 0次 | 1次 |
| 缓存友好 | 是 | 否 |
SSO适用场景:
- 短字符串:文件名、标签、ID等
- 性能敏感:需要频繁创建/销毁字符串的场景
短字符串模式(≤15字符)
1
2
3
4
5
6
7
不分配堆内存,字符串直接存储在32字节的对象中
┌──────────┬──────────┬────────────────────────────────┐
│ 指针无效 │ 长度=5 │ "hello\0" + 未使用空间 │
│ │ │ 16字节 │
└──────────┴──────────┴────────────────────────────────┘
栈内存:32字节
堆内存:0字节
长字符串模式(>15字符)
1
2
3
4
5
6
7
8
9
10
分配堆内存存储字符串
┌──────────┬──────────┬────────────────────────────────┐
│ 堆地址 │ 长度=23 │ 容量 │
│ │ │ │
└──────────┴──────────┴────────────────────────────────┘
↓
堆内存:存储实际字符串"hello world hello world\0"
栈内存:32字节
堆内存:24字节
4. 内存对齐
4.1 内存对齐规则
对齐是为了提高CPU访问内存的效率。
两条规则
- 每个成员的偏移量为成员自身大小的整数倍对齐
- char(1字节):可以从任意地址开始
- short(2字节):地址必须是2的倍数
- int(4字节):地址必须是4的倍数
- double(8字节):地址必须是8的倍数
- 最后进行结构体整体对齐,为最大成员大小的整数倍
4.2 优化内存布局
优化原则:按大小排序成员,大的放前面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不好的做法:浪费空间
struct BadLayout
{
bool flag1; // 1 字节
double value; // 8 字节(需要对齐)
bool flag2; // 1 字节
// 总共:24 字节(包含 padding)
};
// 好的做法:紧凑布局
struct GoodLayout
{
double value; // 8 字节
bool flag1; // 1 字节
bool flag2; // 1 字节
// 总共:16 字节(包含 padding)
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 不好的做法:浪费空间
struct Bad {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
// 实际大小:12字节(1 + 3padding + 4 + 1 + 3padding)
// 好的做法:紧凑布局
struct Good {
int b; // 4字节
char a; // 1字节
char c; // 1字节
// 自动填充2字节
};
// 实际大小:8字节(4 + 1 + 1 + 2padding)
4.3 AOS v.s. SOA
https://zhuanlan.zhihu.com/p/552884861
数据排布方式
- AOS(Array of Structures)
- SOA(Struct of Array)
AOS 更适合面向对象的编程,SOA更适用于需要效率更高的地方
5. 复杂对象的内存
5.1 嵌套容器
原始结构体:
1
2
3
4
5
6
struct ComplexObj {
int a; // 4字节
double b; // 8字节
std::vector<int> inner_vec; // 24字节(对象本身)
std::string str; // 32字节(对象本身)
};
内存层次:
1
2
3
4
5
6
栈上:
ComplexObj对象:72字节
堆上(如果容器非空):
inner_vec的元素:capacity × 4字节
str的字符(如果>15字符):length + 1字节
实际内存布局:
1
2
3
4
5
6
7
8
9
偏移量 成员 大小 说明
----------------------------------------------
0-3 int a 4
4-7 [padding] 4 填充,为了对齐double
8-15 double b 8 必须从8的倍数开始
16-39 vector 24
40-71 string 32
----------------------------------------------
总计:72字节
计算过程:
- int a:4字节(偏移0-3)
- padding:4字节(偏移4-7,为了让double从8开始)
- double b:8字节(偏移8-15)
- vector:24字节(偏移16-39)
- string:32字节(偏移40-71)
- 总计:4 + 4 + 8 + 24 + 32 = 72字节
5.2 Vector存储复杂对象
1
2
std::vector<ComplexObj> vec;
vec.resize(10);
内存占用:
1
2
3
4
5
6
7
8
9
栈上:
vec对象:24字节
堆上:
10个ComplexObj对象:720字节(72×10)
额外堆内存(如果对象内部容器非空):
每个inner_vec的元素
每个str的字符(如果>15字符)