探究内存对齐
回顾:上一章节,我去探究了对象的alloc流程,以及init及new的区别,感兴趣的同学可以去查看我上一章内容。
任务: 上章节中, 在alloc中计算对象所占内存空间时, 有说到字节对齐的知识点, 今天咱们重点来探索它.
准备工作:
po: "expression -O"的简写, 作用打印对象.
p : "expression --"的缩写, 打印返回值的类型以及命令结果的引用名。
x : 用16进制来打印对象内存数据
x/4gx: 格式化打印对象内存数据
bt: 打印当前堆栈信息
- 探索对象属性内存对齐
我们先来看这段代码:
LLPerson *person = [LLPerson alloc];
person.name = @"luln4";
person.nickName = @"LL";
person.age = 28;
person.c1 = 'a';
person.c2 = 'b';
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));
先分析person对象占据了多大内存:
isa: 8个字节, 继承NSObjce而来
name: 8个字节, NSString属性
nickName: 8个自己, NSString属性
age: 4个字节, Int属性
c1: 1个字节, char属性
c2: 1个字节, char属性
我们假设没内存对齐, 那person对象实际占用内存应该是30个字节, 接下来我们来看打印结果:
<LLPerson: 0x103a081e0> - 8 - 32 - 32
为什么呢? 我们断点到NSLog这里, 并在lldb里打印它的内存数据看下 x/4gx 0x103a081e0
(lldb) x/4gx 0x103a081e0
0x6000015815c0: 0x000000010e104778 0x0000001200006261
0x6000015815d0: 0x000000010e102038 0x000000010e102058
我们分别po这些属性, 可以看到0x000000010e104778, 0x000000010e102038, 0x000000010e102058分别打印出LLPerson, luin4, LL, 但比较奇怪的就是属性中的age和c1, c2去哪了?
po 0x0000001200006261是乱码. 我们仔细观察0x0000001200006261, age是4个字节, c1, c2分别是1个字节, 那我们是不是应该分开你去打印呢:
po 0x00000012 /// 28
po 0x62 /// 98 在ASCII码中: b
po 0x61 /// 97 在ASCII码中: a
总结: 看来系统不仅在alloc流程中计算内存大小的源码中, 16位字节对齐(详见size = cls->instanceSize(extraBytes)), 还对内存空间进行了优化 -> 重排 ,大大节省了空间, 不然每个属性都字节对齐占用8个字节, 那得浪费多少内存!
- 探索结构体内存对齐
- 准备工作1 -> 打印内存的三种方式
- sizeof()
- class_getInstanceSize(类对象)
- malloc_size((__bridge const void *)(指针)))
- 准备工作2 -> 各个类型所占空间
sizeof():
1、sizeof是一个运算符, 不是函数
2、我们一般用sizeof计算内存大小时,传入的主要对象是数据类型,这个在编译器的编译阶段(即编译时)就会确定大小而不是在运行时确定。
3、sizeof最终得到的结果是该数据类型占用空间的大小
class_getInstanceSize
runtime API, 计算并返回该对象实际所需空间大小
malloc_size
获取系统实际分配的内存大小(可以结合16字节对齐算法来分析其返回值)
先定义一个结构体
struct LLStruct1 {
int a; /// 4
char b; /// 1
long c; /// 8
short d; /// 2
}struct1;
如上述结构体示例, 根据准备工作2图片, 我给每个成员注释了它所占的空间, 那是不是说这个结构体只需要 15 -> 16 个字节就可以了, 我们来打印看一下:
NSLog(@"%lu",sizeof(struct1)); 输出结果: 24
struct LLStruct1 {
int a; /// 4 0-3
char b; /// 1 4
long c; /// 8 [5,6,7] 8-15
short d; /// 2 16 17
}struct1;
我们先大概猜测一下结构体的<内存对齐>规则:
1: 起始位 是该成员所需内存大小的整倍数;
2: 基于第1点计算出的实际内存大小应转换为 <该结构体> 中 <占用空间最大> 的成员变量的整倍数
我们来验证下:
如图所示, 规则是成立的, 那有人说了, 如果结构体中嵌套了结构体呢? 难道要先计算出 结构体成员 的分配大小, 再来计算该结构体的分配大小呢? 上代码
struct LLStruct4 {
double a; /// 8
int b; /// 4
char c; /// 1
short d; /// 2
}struct4;
///系统给struct4共分配16字节
struct LLStruct3 {
int a;
char b;
long c;
short d;
struct LLStruct4 e;
}struct3;
NSLog(@"%lu",sizeof(struct3)) /// 40
接上图继续画:
e: (16字节) 起始偏移量应为8(struct4 -> a, 结构体内最大成员的大小)的倍数, [24 - 39] -> 40(struct3中最大成员为long和结构体指针, 都为8字节, 所以应为8的倍数)
结构体的<内存对齐>规则总结:
1: 起始位 是该成员所需内存大小的整倍数;
2: 基于第1点计算出的实际内存大小应转换为 <该结构体> 中 <占用空间最大> 的成员变量的整倍数;
3: 当结构体a内嵌套结构体b时, b所存储地址起始偏移位应为b内最大成员大小的整倍数;
最后, 为什么要内存对齐呢?
1: 方便快捷
> 如果不对齐, 那么就会动态自适应读取内存的长度, 这时候消耗大量性能和时间去计算和适配.
2: 安全
> 如果不对齐, 并且没有自适应读取内存, 那么就会出现访问到其他对象的情况,甚至会出现访问到野指针的情况.
* 对齐后, 系统只需要固定读取长度, 以**空间换时间**来读取对象的内存, 就会大大提高访问速度以及安全
* 注意区分x86和64位系统所占空间内存