前言
相信大家都有过便利店和超市的购物经历,大家在购物特别是要买多种商品囤货的时候,大部分人都会选择去大超市吧。便利店的货架通常都是摆满为主,同一个货架上可能放着零食、日用品、饮料,到店里买东西一般都是直接叫老板,自己去找很麻烦。而大超市同一个货架通常是同一类商品,即使摆不满一个货架,那就摆到另一个货架上,同一个货架就算摆不满,但摆的也是同一类商品,毕竟大超市,地方大,顾客购物效率也会高一点。
通常字节是内存的基本单位,但是CPU在操作数据时,常常以块为单位进行存取。如果没有一套规则去约束内存的存放,有些数据就会分布在不同的块区,这样CPU去找这些数据就会很累很烦(效率低)。字节对齐后,CPU找数据就不用跑来跑去(降低存取次数),相对的也就提高了效率。
对象内存的影响因素
接下来上代码分析一下,首先声明一个类:
@interface SLPerson : NSObject
@property(strong,nonatomic)NSString * name;
@property(assign,nonatomic)int age;
@end
研究对象类型的内存大小、对象实际的内存大小以及系统分配内存的大小:
SLPerson * p = [SLPerson alloc];
p.name = @"person";
p.age = 18;
SLPerson * p1;
NSLog(@"p对象类型的内存大小--%lu",sizeof(p));
NSLog(@"p对象实际的内存大小--%lu",class_getInstanceSize([p class]));
NSLog(@"p系统分配的内存大小--%lu",malloc_size((__bridge const void *)(p)));
NSLog(@"==================");
NSLog(@"p1对象类型的内存大小--%lu",sizeof(p1));
NSLog(@"p1对象实际的内存大小--%lu",class_getInstanceSize([p1 class]));
NSLog(@"p1系统分配的内存大小--%lu",malloc_size((__bridge const void *)(p1)));
2022-07-01 10:38:26.985044+0800 alloc分析[29679:2671728] p对象类型的内存大小--8
2022-07-01 10:38:26.985110+0800 alloc分析[29679:2671728] p对象实际的内存大小--24
2022-07-01 10:38:26.985138+0800 alloc分析[29679:2671728] p系统分配的内存大小--32
2022-07-01 10:38:26.985164+0800 alloc分析[29679:2671728] ==================
2022-07-01 10:38:26.985186+0800 alloc分析[29679:2671728] p1对象类型的内存大小--8
2022-07-01 10:38:26.985207+0800 alloc分析[29679:2671728] p1对象实际的内存大小--0
2022-07-01 10:38:26.985231+0800 alloc分析[29679:2671728] p1系统分配的内存大小--0
分析
- 对象类型的内存:p和p1本质就是个指针,所以sizeof(p)和sizeof(p1)都是8字节
- 对象实际的内存:由类的成员变量大小决定,name(8)、age(4)以及isa指针(8),8+4+8=20,嗯???不对啊,明明输出了24啊
- 系统分配的内存:32,这。。。。。。越来越大了呢?后面我们会探究malloc来进行分析
- 至于p1并没有初始化,所以对象的内容所占用的大小均为0
下面我们通过LLDB指令去查看对象p属性在内存中的显示
我们可以看到,p的内容有3个8字节构成,尽管age只占4字节,但是前面4字节也补0了,即对象的内容大小进行了8字节对齐。也就解释了为什么p的内容加起来是20字节,却占了24字节。
下面我们给类SLPerson添加一个实例方法和类方法
@interface SLPerson : NSObject
@property(strong,nonatomic)NSString * name;
@property(assign,nonatomic)int age;
-(void)test1;
+(void)test2;
@end
同样输出
2022-07-01 10:41:39.059577+0800 alloc分析[29700:2673212] p对象类型的内存大小--8
2022-07-01 10:41:39.059640+0800 alloc分析[29700:2673212] p对象实际的内存大小--24
2022-07-01 10:41:39.059668+0800 alloc分析[29700:2673212] p系统分配的内存大小--32
2022-07-01 10:41:39.059689+0800 alloc分析[29700:2673212] ==================
2022-07-01 10:41:39.059712+0800 alloc分析[29700:2673212] p1对象类型的内存大小--8
2022-07-01 10:41:39.059733+0800 alloc分析[29700:2673212] p1对象实际的内存大小--0
2022-07-01 10:41:39.059758+0800 alloc分析[29700:2673212] p1系统分配的内存大小--0
总结
- 成员变量、属性会影响类的实例对象的内存大小。
- 添加方法,对类的实例对象内存大小没有任何影响,方法不存在对象内。
- 在添加成员变量的过程中,由于成员变量的数据类型是不一致的,向最大数据类型的成员变量对齐。继承自NSObject对象的类,默认字节对齐方式是8字节。
结构体内存对齐
对象的本质其实是结构体,内存对齐实际上可以看做是结构体的内存对齐,接下来探究下结构体内存对齐。
无嵌套
以上结果可知,两个结构体的成员变量是一摸一样的,只是声明的顺序不一样,输出的sizeof也不一样了。
分析
下面我们用f(x,y)来模拟成员变量的存储情况,其中x表示成员变量的初始位置,y表示成员变量的大小
SLStruct1:
- a:占8字节,a = f(0,8)
- b:占4字节,(0+8)%4 = 0,b = f(8,4)
- c:占2字节,(8+4)%2 = 0,c = f(12,2)
- d:占1字节,(12+2)%1 = 0,d = f(14,1)
SLStruct2:
- a:占8字节,a = f(0,8)
- d:占1字节,(0+8)%1 = 0,b = f(8,1)
- b:占4字节,(8+1)%4 = 1,需往前移3个位置即到12,12可以被4整除,c = f(12,4)
- d:占2字节,(12+4)%2 = 0,d = f(16,2)
总结
SLStruct1加起来的内存大小是15字节,SLStruct2加起来的内存大小是18字节,两个结构体中成员变量最大内存是8字节,所以SLStruct1与SLStruct2的实际内存大小必须是8的整数倍,自动补齐,向上取整,所以SLStruct1实际大小是16字节,SLStruct2实际大小是24字节。
有嵌套
接下来我们再创建一个结构体,里边嵌套一个结构体
struct SLStruct3{
long a; // 8
int b; // 4
short c; // 2
char d; // 1
struct SLStruct2 lwStr;
}SLStruct3;
打印结果
2022-07-01 11:24:29.010493+0800 哦哦、[2895:94727] SLStruct1-----16
SLStruct2-----24
SLStruct3-----40
Program ended with exit code: 0
分析
同样地,我们来分析SLStruct3
SLStruct1:
- a:占8字节,a = f(0,8)
- b:占4字节,(0+8)%4 = 0,b = f(8,4)
- c:占2字节,(8+4)%2 = 0,c = f(12,2)
- d:占1字节,(12+2)%1 = 0,d = f(14,1)
- lwStr:SLStruct2中成员最大的占8字节,所以(14+1)%8 = 7,需往前移1个位置即到16,16可以被8整除,c = f(16,18)
总结
SLStruct3加起来的内存大小是34字节,结构体中成员变量a和lwStr都占了最大内存是8字节,所以SLStruct3的实际内存大小必须是8的整数倍,自动补齐,向上取整,所以SLStruct3实际大小是40字节。
结构体对齐规则
通过以上实例分析,总结一下结构体是怎么计算大小的
- struct与union的成员,按顺序依次存放,第一个成员初始位置放在x=0的地方,后续成员存放的初始位置需存放在该成员大小的整数倍位置上。
- 假设成员中嵌套有结构体成员s,则s的初始位置要根据s中的最大成员大小来定,比如s中最大的成员占8字节,则s的初始位置需存放在8的整数倍位置上。
- 计算完最后一个成员的位置并加上该成员的大小后(假设为r),要看r是不是结构体中最大成员的整数倍,不足的则要补齐。
- 所以说类的对象计算其内容大小也是按照这个套路,无非就是类的对象比结构体一开始要多算一个isa(占8字节),然后其他成员(属性)往后排放。
malloc探究
malloc_size方法是不直接提供的,这里我下载了libmalloc-317.40.8库去探究,之前在探究alloc时,在经过_class_createInstanceFromZone
方法后,calloc即系统开辟内存,所以我们从calloc开始入手。
1.调用calloc方法
2.跳到calloc
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
3.跳到_malloc_zone_calloc
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(malloc_logger)) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}
4.跳到zone->calloc,发现点不进去,借助汇编。
5.全局搜索 default_zone_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
6.又是zone->calloc,继续汇编
7.全局搜索nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
8.进入_nano_malloc_check_clear,此时我们把焦点放在size_t上,发现了个很好的方法segregated_size_to_fit
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
。。。。。。。
9进入segregated_size_to_fit,此时此刻,我们终于在libmalloc源码找到了核心算法
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
同时我们查看到两个宏定义
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
可以得出,当我们调用calloc(1, 20)时,最后通过算法是通过(20 + 16 - 1) >> 4 << 4 操作 ,结果就是48,即内存对齐按照16字节对齐。(PS:右移一位相当于除以2,左移一位相当于乘以2)
总结
- class_getInstanceSize:获取实例对象所占内存大小(8字节对齐)
- malloc_size:获取系统实际分配的内存大小(16字节对齐)