目录
1.为什么要内存对齐
cpu在访问内存时,并不是逐个字节访问的,而是以字长(word size)
为单位访问。
比如在iOS中32位系统,字长为4字节,也就是每次CPU访问内存以4字节为一个单位长度。
这么设计的目的,是为了减少CPU访问内存的次数,加大CPU访问内存的吞吐量。比如同样读取8个字节的数据,一次读4个字节只需要读2次。
同样也是因为内存条实际上是切片的这种设计,如下图:
实际的:
cpu取数据是按块去读的,如果一位一位的读那么效率会降低非常多;
那么按块读的话,如果不进行内存对齐,当取两个,四个或者八个字节的数据就有可能跨chip。这样cpu就要通过两次寻址找到完整数据,并对数据进行拼接,效率上就损失了好多。因此以空间来置换时间,会进行对齐。
2.iOS中的内存对齐字长
我们在上一篇文章中关于alloc的分析中遇到过对齐的问题。
源码是这样的:
#ifdef __LP64__
#define WORD_SHIFT 3UL
#define WORD_MASK 7UL
#define WORD_BITS 64
#else
#define WORD_SHIFT 2UL
#define WORD_MASK 3UL
#define WORD_BITS 32
#endif
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
- 在iOS中
64位
系统下,是按照8字节
对齐的 - 在iOS中非
64位
系统下,是按照4字节
对齐的
3.对象影响内存大小的因素有什么
首先来认识一下下面的几个内存相关的方法,方便我们分析内存大小的影响因素。
- sizeof
sizeof
是C++
中的运算符,作用是返回变量、对象、以及数据类型所占内存的字节数,它返回的大小和系统相关。作用于基本类型,返回基本类型变量的字节大小;作用于自定义类型,返回自定义类型及变量的大小。
- class_getInstanceSize
Returns the size of instances of a class
这个方法是由runtime
提供的获取类的实例所占用的内存大小。
- malloc_size
/* Returns size of given ptr */
返回传入的指针所指向
的内存空间的大小。
搞清楚上面的👆🏻这几个方法,我们分别来通过增加,属性、方法、协议、还有分类,通过打印来观察下对开辟的内存的影响。
属性
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
LGPerson *person = [LGPerson alloc];
person.name = @"NiuNiu";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size(( __bridge const void )(person)));
打印结果:
系统内存开辟分析[56214:11947755] <LGPerson: 0x600000014000> - 8 - 16 - 16
根据打印结果:可以看到
- 一个对象类型所占的字节数是8个;
- 实例person占用的内存空间大小是16个字节;
- person指向的内存空间大小是16个字节;
我们给类LGPerson
增加一个属性,然后看看内存的变化,增加了一个
@property (nonatomic, copy) NSString *nickName;
系统内存开辟分析[56367:11960749] <LGPerson: 0x600000205380> - 8 - 24 - 32
发生了变化
:
占用的内存大小增加了8个,变成了24;person指向的内存空间增加了16变成了32;
继续
,我们增加属性
@property (nonatomic, assign) int age;
此时的打印:
系统内存开辟分析[56437:11966024] <LGPerson: 0x600000201ba0> - 8 - 32 - 32
占用的内存大小增加了8个,变成了32;person指向的内存空间和上一次比没变还是32;
先不说这个内存开辟的规律是什么,后面会说这个对齐的规则和原因。我们可以通过这个打印看到,属性
是对开辟内存的大小有影响的;
-
方法
我们继续在类LGPerson
中增加方法来看看方法对内存的影响,增加了方法
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
- (void)run;
@end
@implementation LGPerson
- (void)run{
NSLog(@"niuniu go run!");
}
@end
结果:
系统内存开辟分析[56565:11976327] <LGPerson: 0x6000002055e0> - 8 - 32 - 32
并没有发生变化,我再依次增加了几个方法,或者是类方法,这个打印的大小都不会发生变化。
协议
也是没有变化的。
分类
嗯,也没什么变化。
结论
方法、协议、还有分类这几个对对象内存的开辟无影响。
4.内存对齐的规则
-
数据成员对齐规则:结构或联合的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者子成员大小的整数倍开始。
- 例如:
int
是四个字节,那么要从4
的整数倍地址开始存储。
- 例如:
-
结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
- 例如:struct a里有struct b,b里有char,int,double等元素,那么b要从8的倍数的位置开始存。
结构体总大小,也就是
sizeof
的结果,必须是其内部最大成员的整数倍。不足要补齐。
基本数据类型占内存大小
- 实战演练
struct Struct1 {
double a; // [0,7]
char b; // [8]
int c; // 根据规则一要从4的倍数开始,所以[12,13,14,15]。跳过9,10,11
short d; //[16,17]
}struct1;
//根据第三准则总大小要是8的倍数,那就要分配24字节。
struct Struct2 {
double a; //[0,7]
int b; //[8,11]
char c; //[12]
short d; //根据规则一跳过13,从14开始 [14,15]
}struct2;
//这里0~15大小本来就为16了,所以不需要补齐了。
5.相关源码探索
我们在上一节课分析alloc
的流程里,实际开辟内存空间的代码是:
是方法calloc
方法,它在objc
中的只有一个声明,没有具体的实现
void *calloc(size_t __count, size_t __size) __result_use_check __alloc_size(1,2);
它的实现在libmalloc
中。继续查看源码来看具体的实现。
- calloc
它的实现如下:
void * calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
主要调用的方法是_malloc_zone_calloc
,实现如下:
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
void *ptr;
if (malloc_check_start) {
internal_check();
}
// Cooci 和谐学习不急不躁 这个流程是有优化的 会和苹果原系统不一定完全重合
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(malloc_instrumented || malloc_check_start ||
malloc_logger || zone->version < 13)) {
return _malloc_zone_calloc_instrumented_or_legacy(zone, num_items, size, mzo);
}
return zone->calloc(zone, num_items, size);
}
这里核心的调用是calloc
,我们进一步点进去进入到它的实现是:
void* (* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size);
/* same as malloc, but block returned is set to zero */
发现没有下面的实现了,我们通过po/p
的方式来打印下实际的下一步函数的指向,(我理解的是实际存储的其实就是下一步要跳转到的方法地址)。
来到了(.dylib'default_zone_calloc at malloc.c:504)
,也就是方法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, size);
}
我们通过同样的方式,找到了default_zone_calloc
中的zone->calloc
指向,(.dylib'szone_calloc at magazine_malloc.c:322)
,找到了szone_calloc
,继续跟进,找到方法
void *
szone_calloc(szone_t *szone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
return szone_malloc_should_clear(szone, total_bytes, 1);
}
找到核心方法szone_malloc_should_clear
,进一步查看
MALLOC_NOINLINE void *
szone_malloc_should_clear(szone_t *szone, size_t size, boolean_t cleared_requested)
{
void *ptr;
msize_t msize;
if (size <= TINY_LIMIT_THRESHOLD) {
// size + 15 >> 4 << 4 (size 16字节对齐)
// (size + (1 << 4) - 1) >> 4 ?????? = (size + 16-1)>> 4
// #define SHIFT_TINY_QUANTUM 4ull
msize = TINY_MSIZE_FOR_BYTES(size + TINY_QUANTUM - 1);
if (!msize) {
msize = 1;
}
// MALLOC_TRACE(TRACE_tiny_malloc, (uintptr_t)rack, TINY_BYTES_FOR_MSIZE(msize), (uintptr_t)tiny_mag_ptr, cleared_requested);
// #define TINY_BYTES_FOR_MSIZE(_m) ((_m) << SHIFT_TINY_QUANTUM)
// (size + (1 << 4) - 1) >> 4 ?????? = (size + 16-1)>> 4 << 4
ptr = tiny_malloc_should_clear(&szone->tiny_rack, msize, cleared_requested);
} else if (size <= SMALL_LIMIT_THRESHOLD) {
//(size + 1<<9 -1 )>>9<<9
msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
if (!msize) {
msize = 1;
}
ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
#if CONFIG_MEDIUM_ALLOCATOR
} else if (szone->is_medium_engaged && size <= MEDIUM_LIMIT_THRESHOLD) {
msize = MEDIUM_MSIZE_FOR_BYTES(size + MEDIUM_QUANTUM - 1);
if (!msize) {
msize = 1;
}
ptr = medium_malloc_should_clear(&szone->medium_rack, msize, cleared_requested);
#endif
} else {
size_t num_kernel_pages = round_large_page_quanta(size) >> large_vm_page_quanta_shift;
if (num_kernel_pages == 0) { /* Overflowed */
ptr = 0;
} else {
ptr = large_malloc(szone, num_kernel_pages, 0, cleared_requested);
}
}
#if DEBUG_MALLOC
if (LOG(szone, ptr)) {
malloc_report(ASL_LEVEL_INFO, "szone_malloc returned %p\n", ptr);
}
#endif
/*
* If requested, scribble on allocated memory.
*/
if ((szone->debug_flags & MALLOC_DO_SCRIBBLE) && ptr && !cleared_requested && size) {
memset(ptr, SCRIBBLE_BYTE, szone_size(szone, ptr));
}
if (os_unlikely(!ptr)) {
malloc_set_errno_fast(MZ_POSIX, ENOMEM);
}
return ptr;
}
这个分为三种size:tiny
、small
和large
,我们以small
为例进行具体的分析。
if (size <= SMALL_LIMIT_THRESHOLD) {
// (size + (1<<9) -1 )>>9
msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
if (!msize) {
msize = 1;
}
// (size + 511 )>>9 << 9
ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
}
前面的SMALL_MSIZE_FOR_BYTES
是对齐的一个操作,是512,重点来看方法small_malloc_should_clear
small_malloc_should_clear
加了一小部分的注释,这个方法我也只看懂了一小部分,了解了大概
void *
small_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
void *ptr;
//获取 magazine_t
mag_index_t mag_index = small_mag_get_thread_index() % rack->num_magazines;
magazine_t *small_mag_ptr = &(rack->magazines[mag_index]);
MALLOC_TRACE(TRACE_small_malloc, (uintptr_t)rack, SMALL_BYTES_FOR_MSIZE(msize), (uintptr_t)small_mag_ptr, cleared_requested);
//加锁
SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);
//如果配置了缓存
#if CONFIG_SMALL_CACHE
//取上一次释放的空间
ptr = small_mag_ptr->mag_last_free;
// 如果上一次刚释放出来的空间 和 传进来的大小正好相同 直接返回
if (small_mag_ptr->mag_last_free_msize == msize) {
// we have a winner 很强 很幸运 😊
small_mag_ptr->mag_last_free = NULL;
small_mag_ptr->mag_last_free_msize = 0;
small_mag_ptr->mag_last_free_rgn = NULL;
SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
}
return ptr;
}
#endif /* CONFIG_SMALL_CACHE */
//没开启缓存,直接从small_malloc_from_free_list中读
while (1) {
//会匹配到一块和msize大小相同的空间的地址返回
ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
if (ptr) {
SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
}
return ptr;
}
#if CONFIG_RECIRC_DEPOT
//支持存储,再试一次从small_malloc_from_free_list中读
if (small_get_region_from_depot(rack, small_mag_ptr, mag_index, msize)) {
ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
if (ptr) {
SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, SMALL_BYTES_FOR_MSIZE(msize));
}
return ptr;
}
}
#endif // CONFIG_RECIRC_DEPOT
// The magazine is exhausted. A new region (heap) must be allocated to satisfy this call to malloc().
// The allocation, an mmap() system call, will be performed outside the magazine spin locks by the first
// thread that suffers the exhaustion. That thread sets "alloc_underway" and enters a critical section.
// Threads arriving here later are excluded from the critical section, yield the CPU, and then retry the
// allocation. After some time the magazine is resupplied, the original thread leaves with its allocation,
// and retry-ing threads succeed in the code just above.
//以上操作都没有读到合适的内存空间,那么需要额外开辟新的heap去满足这次开辟的申请
if (!small_mag_ptr->alloc_underway) {
void *fresh_region;
// time to create a new region (do this outside the magazine lock)
small_mag_ptr->alloc_underway = TRUE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
//申请新的page
fresh_region = mvm_allocate_pages(SMALL_REGION_SIZE,
SMALL_BLOCKS_ALIGN,
MALLOC_FIX_GUARD_PAGE_FLAGS(rack->debug_flags),
VM_MEMORY_MALLOC_SMALL);
SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);
// DTrace USDT Probe
MAGMALLOC_ALLOCREGION(SMALL_SZONE_FROM_RACK(rack), (int)mag_index, fresh_region, SMALL_REGION_SIZE);
//内存溢出了 返回空
if (!fresh_region) { // out of memory!
small_mag_ptr->alloc_underway = FALSE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
return NULL;
}
region_set_cookie(®ION_COOKIE_FOR_SMALL_REGION(fresh_region));
//新申请的page通过hash插入到固定的位置
// small_malloc_from_region_no_lock 内部包括插入rack_region_insert
// magazine_t的结构是一个双向链表 ,可以进一步查看下他的数据结构
//small_malloc_from_region_no_lock 方法中也有对新开辟的空间的双向链表的插入处理 recirc_list_splice_last
ptr = small_malloc_from_region_no_lock(rack, small_mag_ptr, mag_index, msize, fresh_region);
// we don't clear because this freshly allocated space is pristine
small_mag_ptr->alloc_underway = FALSE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
return ptr;
} else {
SZONE_MAGAZINE_PTR_UNLOCK(small_mag_ptr);
yield();
SZONE_MAGAZINE_PTR_LOCK(small_mag_ptr);
}
}
/* NOTREACHED */
}
首先获取了一个
magazine_t
,它本身是一个双向链表的结构。如果支持了缓存策略,会先从它自己的
mag_last_free
中,读到上一次刚被释放出来的地址,判断和目前申请的msize
是不是一样,如果刚好一样,那么!很好,很幸运,直接把这个地址返回。如果没有那么幸运,那么按部就班让
small_malloc_from_free_list
处理,也就是从magazine_t
->mag_free_list
中进行读取空闲的空间,也就是free_list
。如果支持循环存储,再试一次从
small_malloc_from_free_list
中读取一遍。-
如果上面一系列操作猛如虎,也都没有找到合适的空间地址分配给msize,那么开辟新的page。
- 开辟新的page,首先加锁。
- 判断是否溢出,溢出返回null
- 没有溢出,开辟新的page成功的话,进行插入操作
small_malloc_from_region_no_lock
- 方法
small_malloc_from_region_no_lock
内部通过rack_region_insert
方法,进行应该是hash计算找到合适的位置进行插入。 - 并通过
recirc_list_splice_last
进行新的空间插入到page
中,把双向链表的指针指向进行处理。
把
ptr
返回给msize
,从此msize
有了名字
,就是ptr
的地址~
总结
malloc的过程还是非常严谨的,之所以有这样的过程,依赖于本身的数据结构设计,像magazine_t
这种,本身是一个双向链表,内部还存储了自己的free_list
,一个magazine_t
的结构中带了自己的很多存储信息,开辟信息,上一次释放内容的这样的信息等。
这次对内存对齐的了解比第一次学习的也更加深入了一些,希望再看第三遍,第四遍或者第五遍的时候,能理解的更好。
附上去年写的博客,有个深入的对照:https://www.jianshu.com/p/700833c1140d
参考文章: