002:对象原理中-内存对齐,结构体内存对齐: sizeof class_getInstanceSize malloc_size
问题
1:为什么要内存对齐
2:为什么要内存重排
3:内存计算
目录
1:为什么要内存对齐
2:内存对齐规则
3:单个结构体内存对齐
4:结构体嵌套结构体
5: 内存优化
6:属性重排
7:instanceSize方法的底层实现
8:8字节和16字节对齐
预备知识
1:lldb调试指令
po 打印信息
p 打印详细的信息
bt 打出堆
register read 读取寄存器
x 读取内存段
x/4gx 读取4段内存段
2:计算内存大小的三种方法
LWPerson * p = [LWPerson alloc]; LWPerson * q; NSLog(@"对象类型占用内存大小--%lu", (p)); NSLog(@"对象类型占用内存大小--%lu",sizeof(q)); NSLog(@"对象实际内存大小-----%自定义多种数据类型属性的Person对象",class_getInstanceSize([p class])); NSLog(@"对象实际内存大小-----%lu",class_getInstanceSize([q class])); NSLog(@"系统为分配的内存大小--%lu",malloc_size((__bridge const void *)(p))); NSLog(@"系统为分配的内存大小--%lu",malloc_size((__bridge const void *)(q)));
2.2:打印结果如下
2020-09-19 22:59:42.507118+0800 KCObjcTest[4003:298596] 对象类型占用内存大小--8 2020-09-19 22:59:42.507678+0800 KCObjcTest[4003:298596] 对象类型占用内存大小--8 2020-09-19 22:59:42.507780+0800 KCObjcTest[4003:298596] 对象实际内存大小-----8 2020-09-19 22:59:42.507844+0800 KCObjcTest[4003:298596] 对象实际内存大小-----0 2020-09-19 22:59:42.507910+0800 KCObjcTest[4003:298596] 系统分配的内存大小--16 2020-09-19 22:59:42.507963+0800 KCObjcTest[4003:298596] 系统分配的内存大小--0
2.3:总结
sizeof
传进来的是类型,用来计算这个类型占多大内存,这个在编译器编译阶段就会确定,所以sizeof(p)
和sizeof(q)
的结果都是一样的,p
和q
都是指针类型,指针大小就是8个字节
。
class_getInstanceSize
对象的实际内存大小,大小由类的属性和变量来决定,实际上并不是严格意义上的对象内存大小,因为底层进行8字节对齐
算法define WORD_MASK 7UL
((x + WORD_MASK) & ~WORD_MASK
,LWPerson
类中没有其他的属性和变量,但是继承了NSObject
,NSObject
中有一个isa
指针,所以内存大小是8字节
malloc_size
系统分配的内存大小是按16字节对齐
的方式,即是按16的倍数
分配 ,不足则系统会自动填充字节(具体的calloc详细流程后续会更新
3:各类型所占字节
OC中的NSInteger 和int 有什么区别
在32位操作系统时候, NSInteger 等价于 int,即32位
在64位操作系统时候, NSInteger 等价于 long,即64位
正文-内存对齐
1:为什么要内存对齐
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
3、以空间换时间
参考: 为什么要进行内存对齐?
2:内存对齐规则
1、数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。
min(m, n)
的公式, 其中 m
表示当前成员的开始位置
, n
表示当前成员所需要的位数
。如果满足条件 m 整除 n
(即 m % n == 0
), n
从 m
位置开始存储, 反之继续检查 m+1 能否整除 n
, 直到可以整除, 从而就确定了当前成员的开始位置。2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补⻬
3:单个结构体内存对齐
3.1:结构体MyStruct1 内存大小计算
根据内存对齐规则计算MyStruct1
的内存大小,详解过程如下:
变量a
:占1
个字节,从0开始,此时min(0,1)
,即0 存储 a
变量b
:占8
个字节,从1开始,此时min(1,8)
,1不能整除8,继续往后移动,知道min(8,8)
,从8开始,即8-15 存储 b
变量c
:占4
个字节,从16开始,此时min(16,4)
,16可以整除4,即16-19 存储 c
变量d
:占2
个字节,从20开始,此时min(20, 2)
,20可以整除2,即20-21 存储 d
因此MyStruct1
的需要的内存大小为 15
字节,而MyStruct1中最大变量的字节数为8
,所以 MyStruct1 实际的内存大小必须是 8 的整数倍
,18向上取整到24,主要是因为24是8的整数倍,所以 sizeof(MyStruct1) 的结果是 24
3.2:结构体MyStruct2 内存大小计算
根据内存对齐规则计算MyStruct2
的内存大小,详解过程如下:
变量b
:占8
个字节,从0开始,此时min(0,8)
,即0-7 存储 b
变量c
:占4
个字节,从8开始,此时min(8,4)
,8可以整除4,即8-11 存储 c
变量d
:占2
个字节,从12开始,此时min(12, 2)
,12可以整除2,即12-13 存储 d
变量a
:占1
个字节,从14开始,此时min(14,1)
,即14 存储 a
因此MyStruct2
的需要的内存大小为 15
字节,而MyStruct1中最大变量的字节数为8
,所以 MyStruct2 实际的内存大小必须是 8 的整数倍
,15向上取整到16,主要是因为16是8的整数倍,所以 sizeof(MyStruct2) 的结果是 16、
引用:单个结构体内存大小
4:结构体嵌套结构体
结构体中嵌套结构体的内存大小计算情况
//1、结构体嵌套结构体 struct Mystruct3{ double b; //8字节 int c; //4字节 short d; //2字节 char a; //1字节 struct Mystruct2 str; }Mystruct3; //2、打印 Mystruct3 的内存大小 NSLog(@"Mystruct3内存大小:%lu", sizeof(Mystruct3)); NSLog(@"Mystruct3中结构体成员内存大小:%lu", sizeof(Mystruct3.str));
分析Mystruct3
的内存计算
-
根据内存对齐规则,来一步一步分析
Mystruct3
内存大小的计算过程变量b
:占8
个字节,从0开始,此时min(0,8)
,即0-7 存储 b
变量c
:占4
个字节,从8开始,此时min(8,4)
,8可以整除4,即8-11 存储 c
变量d
:占2
个字节,从12开始,此时min(12, 2)
,20可以整除2,即12-13 存储 d
变量a
:占1
个字节,从14开始,此时min(14,1)
,即14 存储 a
结构体成员str
:str是一个结构体,根据内存对齐原则二
,结构体成员要从其内部最大成员大小的整数倍开始存储
,而MyStruct2
中最大的成员
大小为8
,所以str要从8的整数倍开始,当前是从15开始,所以不符合要求,需要往后移动到16,16是8的整数倍,符合内存对齐原则,所以16-31 存储 str
因此MyStruct3
的需要的内存大小为 32
字节,而MyStruct3中最大变量为str, 其最大成员内存字节数为8
,根据内存对齐原则,所以 MyStruct3 实际的内存大小必须是 8 的整数倍
,32正好是8的整数倍,所以 sizeof(MyStruct3) 的结果是 32
5: 内存优化-->>结构体内存大小与结构体成员内存大小的顺序有关
MyStruct1 通过内存字节对齐原则,增加了9个字节,而MyStruct2通过内存字节对齐原则,通过4+2+1的组合,只需要补齐一个字节即可满足字节对齐规则,这里得出一个结论结构体内存大小与结构体成员内存大小的顺序有关
-
如果是
结构体中数据成员
是根据内存从小到大
的顺序定义的,根据内存对齐规则来计算结构体内存大小,需要增加有较大的内存padding即内存占位符,才能满足内存对齐规则,比较浪费内存
-
如果是
结构体中数据成员
是根据内存从大到小
的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding
即可满足堆存对齐规则,这种方式就是苹果中采用的
,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的
苹果中属性重排
,即内存优化
6:属性重排
1:定义一个自定义CJLPerson类,并定义几个属性
@interface CJLPerson : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *nickName; // @property (nonatomic, copy) NSString *hobby; @property (nonatomic, assign) int age; @property (nonatomic, assign) long height; @property (nonatomic) char c1; @property (nonatomic) char c2; @end @implementation CJLPerson @end
2:在main中创建CJLPerson的实例对象,并对其中的几个属性赋值
int main(int argc, char * argv[]) { @autoreleasepool { CJLPerson *person = [CJLPerson alloc]; person.name = @"CJL"; person.nickName = @"C"; person.age = 18; person.c1 = 'a'; person.c2 = 'b'; NSLog(@"%@",person); } return 0; }
3:断点调试
当我们向通过0x0000001200006261
地址找出age等数据时,发现是乱码,这里无法找出值的原因是苹果中针对age、c1、c2属性的内存进行了重排
,因为age类型占4个字节,c1和c2类型char分别占1个字节,通过4+1+1
的方式,按照8字节对齐
,不足补齐的方式存储在同一块内存
中,
- age的读取通过
0x00000012
- c1的读取通过
0x61
(a的ASCII码是97) - c2的读取通过
0x62
(b的ASCII码是98)
4:CJLPerson的内存分布情况
注意:
1、char类型的数据读取出来是以ASCII码的形式显示
2、图片中地址为0x0000000000000000
,表示person中还有属性未赋值
这里可以总结下苹果中的内存对齐思想:
- 大部分的内存都是通过固定的内存块进行读取,
- 尽管我们在内存中采用了内存对齐的方式,但
并不是所有的内存都可以进行浪费的
,苹果会自动对属性进行重排,以此来优化内存
其实内存对齐只是制定了一套规则,目的是提高cpu的读取效率和安全的访问,通过字节对齐虽然这样浪费了大量的内存,但是同时又进行内存优化尽可能的降低了内存了浪费
7:instanceSize方法的底层实现
首先需要知道,对象的内存空间里存放的是对象的属性,所以计算内存大小即为计算所有属性所占的内存大小,先看下objc源码的底层实现:
1:instanceSize
//objc-781源码中的instanceSize方法
size_t instanceSize(size_t extraBytes) const { //方式一:编译器快速计算内存大小(16字节对齐) if (fastpath(cache.hasFastInstanceSize(extraBytes))) { return cache.fastInstanceSize(extraBytes); } //方式二:计算类中所有属性和方法的内存占用 + 额外的字节数0(8字节对齐) size_t size = alignedInstanceSize() + extraBytes; //CF requires all objects be at least 16 bytes. //最少申请16字节的内存大小 if (size < 16) size = 16; return size; }
上面两个方式的区别,在于将对象所有属性所占的内存大小采取不同的对齐方式,方式一是16字节对齐,方式二是8字节对齐。
注意,在不同版本的objc源码中,instanceSize
方法的底层实现可能是不一样的,本文参照的是最新的objc-781
源码,里面会有这两种计算方式,但是实际运行,其实是以方式一来计算的,16字节内存对齐。而在老版本的objc源码中,比如objc-750
源码,里面只有方式二的计算方式,采取的是8字节对齐。
//objc-750源码中的instanceSize方法 size_t instanceSize(size_t extraBytes) { size_t size = alignedInstanceSize() + extraBytes; // CF requires all objects be at least 16 bytes. if (size < 16) size = 16; return size; }
2:fastInstanceSize
size_t fastInstanceSize(size_t extra) const { ASSERT(hasFastInstanceSize(extra)); if (__builtin_constant_p(extra) && extra == 0) { return _flags & FAST_CACHE_ALLOC_MASK16; } else { size_t size = _flags & FAST_CACHE_ALLOC_MASK; // remove the FAST_CACHE_ALLOC_DELTA16 that was added // by setFastInstanceSize return align16(size + extra - FAST_CACHE_ALLOC_DELTA16); } }
3:align16: 编译器快速计算出需要的size后,最后会执行align16
方法,即『16字节对齐』
static inline size_t align16(size_t x) { return (x + size_t(15)) & ~size_t(15); }
系统在计算出内存size后,会先加15,再与个非15,这样就实现了16字节对齐。接下来通过两组二进制运算来直观地看看字节对齐后的结果:
class_getInstanceSize方法最终实现就两步,先获取到对象属性所占的内存大小.再将内存大小进行8字节或者16字节对齐对齐运算。
instanceSize
方法计算需要申请多大的内存空间,再在calloc
方法里对申请的内存大小进行16字节对齐处理,然后按处理后的结果来给对象分配内存空间,并返回内存地址。系统最终实际为对象分配的内存空间大小为16字节的整数倍,并且最少16字节,如果instanceSize
方法里是按16字节对齐的,那实际分配的内存大小和申请的内存大小相同,如果是按8字节对齐,则不同。8:8字节和16字节对齐
8.1: 不含任何自定义属性的Person对象
//不含自定义属性的Person对象 @interface Person : NSObject @end //创建Person对象,打印内存大小 Person *person = [[Person alloc] init]; NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class])); //打印结果 size16 = 16 //16字节对齐的结果 size8 = 16 //8字节对齐的结果 class_getInstanceSize = 8 //获取的8字节对齐的内存
person对象里只有一个isa属性,占8字节,那属性总共占用8字节的内存大小:
size16 = 16
:经由16字节对齐后,结果为16字节。size8 = 16
:经由8字节对齐后,结果为8字节。但oldInstanceSize方法最后对内存大小进行了判断,如果小于16,就返回16。class_getInstanceSize = 8
:这个方法只作了8字节对齐,所以返回为8。
//声明两个指针类型的属性 @interface Person : NSObject @property (nonatomic, strong) NSString *name; //8 @property (nonatomic, strong) NSString *sex; //8 @end //创建Person对象,打印内存大小 Person *person = [[Person alloc] init]; NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class])); //打印结果 size16 = 32 //16字节对齐的结果 size8 = 24 //8字节对齐的结果 class_getInstanceSize = 24 //获取的8字节对齐的内存
person对象里有三个指针类型的数据(isa,name,sex),属性总共占用24字节的内存大小:
size16 = 32
:经由16字节对齐后,结果为32字节。size8 = 24
:经由8字节对齐后,结果为24字节。class_getInstanceSize = 24
:这个方法只作了8字节对齐,所以返回为24。
//Person类里自定义了多种类型的属性 @interface Person : NSObject @property (nonatomic, strong) NSString *name; //8 @property (nonatomic, strong) NSString *sex; //8 @property (nonatomic, assign) int age; //4 @property (nonatomic, assign) long height; //8 @property (nonatomic) char c1; //1 @property (nonatomic) char c2; //1 @end //创建Person对象,打印内存大小 Person *person = [[Person alloc] init]; NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class])); //打印结果 size16 = 48 //16字节对齐的结果 size8 = 40 //8字节对齐的结果 class_getInstanceSize = 40 //获取的8字节对齐的内存
person对象里的属性总共占用38字节的内存大小:
size16 = 48
:经由16字节对齐后,结果为48字节。size8 = 40
:经由8字节对齐后,结果为40字节。class_getInstanceSize = 40
:这个方法只作了8字节对齐,所以返回为40。
8.4: 对象里包含结构体成员属性
//声明一个结构体struct1,根据前面的分析可知,内存为24。 struct Struct1 { double a; // 8 char b; // 1 int c; // 4 short d; // 2 }; //在person最后面添加一个struct1的类 @interface Person : NSObject @property (nonatomic, strong) NSString *name; //8 @property (nonatomic, assign) int age; //4 @property (nonatomic, assign) long height; //8 @property (nonatomic) char c1; //1 @property (nonatomic) struct Struct1 str; //24 @end //创建Person对象,打印内存大小 Person *person = [[Person alloc] init]; NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class])); //打印结果 size16 = 64 //16字节对齐的结果 size8 = 56 //8字节对齐的结果 class_getInstanceSize = 56 //获取的8字节对齐的内存
person对象其他属性占用了29个字节,在对象内存里存放在位置0-28 。结构体str成员,本应该在位置29处开始存放,但根据结构体内存对齐规则,当结构体作为成员时,则结构体成员要从其内部最⼤元素所占内存⼤⼩的整数倍地址开始存储,结构体str里最大成员占用8字节内存,所以str需要从8的整数倍开始存储,即从位置32开始存放,存放位置为32-55,所以最后person对象需要56字节内存:
size16 = 64
:经由16字节对齐后,结果为64字节。size8 = 56
:经由8字节对齐后,结果为56字节。class_getInstanceSize = 56
:这个方法只作了8字节对齐,所以返回为56。
通过分析上述示例的打印结果,均可印证前面说的内存大小的计算规则:先统计所有属性所占的内存大小,然后再进行16字节对齐。简单数据类型的属性所占内存比较容易统计,但对于比较复杂的数据结构,比如说结构体(struct
),它的内存计算就需要遵循『结构体内存对齐规则』。
注意