之前通过 objc
的源码探索了 alloc
的内部流程,到最后会调用 size = cls->instanceSize(extraBytes);
方法,获取内存大小,但是这个大小到底是怎么计算的呢?
获取大小后,会调用 calloc(1, size)
方法开辟内存大小,开辟的时候又有什么不同呢?
这次就继续探索一下系统的内存分配。
一、属性所占内存计算
从应用代码开始
@interface GLPerson : NSObject
// 会有一个隐藏属性 isa 占8个字节
@property (nonatomic, copy ) NSString *name; // 8
@property (nonatomic, assign) int height; // 4
@property (nonatomic, assign) char char1; // 1
@property (nonatomic, assign) char char2; // 1
@en
---
GLPerson *p = [[GLPerson alloc] init];
p.height = 180;
p.name = @"loong";
p.char1 = 'g';
p.char2 = 'n';
NSLog(@"%zd %zd", class_getInstanceSize([GLPerson class]), malloc_size((__bridge const void *)(p)));
上面会输出:24 32
在alloc流程中简单说过,属性内存分配的时候是8字节对齐,GLPerson类的实例所占大小计算为 8 (isa) + 8 (name) + 4 (height) + 1 (char1) + 1 (char1) == 22
。(模拟计算结果,实际在跟源码的时候苹果有内存优化,会把 4 (height) + 1 (char1) + 1 (char1)
放到一个8字节里面存储,这样避免了浪费)
然后会对22进行8字节对齐,得到的是24。
// 1
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// 2
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
// 3
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
// 4
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
根据上面4个方法的调用顺序
-
class_getInstanceSize
: 内部调用alignedInstanceSize
返回 -
alignedInstanceSize
: 内部调用word_align(unalignedInstanceSize())
返回 -
unalignedInstanceSize
: 内部调用data()->ro()->instanceSize
返回,data()
和ro()
的数据会在loadImage
的时候完成,instanceSize会根据类有多少属性,返回已经经过编译器优化存储后的结果 -
word_align
: 这个会对实例大小做8字节对齐,会返回8的倍数大小
1.1 对齐计算
word_align
就是8字节对齐计算
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
可以看出word_align的对齐计算是 (x + 7) & ~7
,
1.2 计算结构体大小
咱们知道OC继承与C,并且 struct objc_class : objc_object
, objc_class
的源码也是一个结构体,结构体在计算大小的时候有3个原则:
1、每个成员的偏移量都必须是当前成员所占内存大小的整数倍如果不是编译器会在成员之间加上填充字节。
2、结构体作为成员: 如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。(struct a⾥存有struct b,b⾥有char,int ,double等元素,那b应该从8的整数倍开始存储)
3、当所有成员大小计算完毕后,编译器判断当前结构体大小是否是结构体中最宽的成员变量大小的整数倍 如果不是会在最后一个成员后做字节填充。
struct structA {
long height; // 8
int age; // 4
char char1; // 1
short short1; // 2
};
struct structB {
int age; // 4
long height; // 8
char char1; // 1
short short1; // 2
};
struct structC {
int age; // 4
struct structB sb;
char sex; // 1
};
---
struct structA a = {
12, 20, 'a', 123
};
struct structB b = {};
struct structC c = {};
NSLog(@"A:%lu, B:%lu, C:%lu", sizeof(a), sizeof(b), sizeof(c));
---
console: A:16, B:24, C:40
structA:
height 0-->7
;
age 8 --> 11
;
char1 12
;
short1
(根据原则1,第13位不是2的整数倍,往后移,14满足) 14-->15
;
实际总共:0--> 15
为 16,
再根据原则3,需要是8的倍数,16满足,最后就是16。
structB:
age 0 --> 3
;
height
(根据原则1,4不满足8的整数倍,往后移,8满足) 8-->15
;
char1 16
;
short1
(第17位不是2的整数倍,往后移,18满足)18-->19
;
实际总共:0--> 19
为 20,
再根据原则3,需要是8的倍数,所以最后是24。
structC:
age 0 --> 3
;
structB sb
(根据原则2,因为structB里面最大的是8字节,4不满足8的整数倍,往后移,8满足,可知structB占24) 8-->31
;
sex 32
;
实际总共:0--> 32
为 33,
再根据原则3,需要是8的倍数,所以最后是40。
1.3 编译优化
测试如下代码
struct structB {
long isa; // 8
char char1; // 1
int height; // 4
char char2; // 1
double name; // 8
char char3; // 1
char char4; // 1
};
---
@interface GLPerson : NSObject
// 默认属性isa 占8个字节
@property (nonatomic, assign) char char1; // 1
@property (nonatomic, assign) int height; // 4
@property (nonatomic, assign) char char2; // 1
@property (nonatomic, copy ) NSString *name; // 8
@property (nonatomic, assign) char char3; // 1
@property (nonatomic, assign) char char4; // 1
@end
---
struct structB b = {};
LGPerson *p = [[LGPerson alloc] init];
p.char1 = 'a';
p.height = 180;
p.char2 = 'b';
p.name = @"loong";
p.char3 = 'c';
p.char4 = 'd';
NSLog(@"%lu, %zd, %zd",sizeof(b), class_getInstanceSize([LGPerson class]), malloc_size((__bridge const void *)(p)));
console: 40, 24, 32
通过输出结果可以知道,structB大小为40,LGPerson真正占用大小为24。为什么,这里面就是系统底层做了编译优化,在字节对齐的基础上,又节省了空间。可以通过打印内存地址查看
可以使用x 或者 memory read命令查看某个对象的内存情况。
更方便的查看4xg规则
可以通过po命令打印出内存中对应的值如下:
可以知道底层把 height
char1
char2
char3
char4
放到了一个8字节里面。
这样不会像结构体那样按顺序存储,中间会有很多补位。在保证对齐原则的情况下,极大的节省了内存空间。
二、calloc() 源码分析
分析 malloc
的源码,官方地址,本次分析的是libmalloc-283.100.6
版本。
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));
console: 48
为什么开辟了48???从源码里面找下答案
分析 malloc
源码的时候,还是需要配置一个能编译运行的源码工程的,这样断点能走进去,方便分析。
2.1 calloc
从calloc
开始
void *
calloc(size_t num_items, size_t size)
{
void *retval;
// 主流程
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
// 主流程
ptr = zone->calloc(zone, num_items, size);
if (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);
return ptr;
}
calloc
-> malloc_zone_calloc
-> ptr = zone->calloc(zone, num_items, size)
发现 calloc
又调回去了???只看源码的话,确实找不到下一步走向了哪里。
还是需要编译运行工程,通过调用 void *p = calloc(1, 40);
在 ptr = zone->calloc(zone, num_items, size)
打个断点。然后打印下,看看调用方法
也可以通过按住 control
点击 step into
多次,进入下一个方法调用。
可知下一步来到了 default_zone_calloc
2.2 default_zone_calloc 、nano_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);
}
同样的方式,来到 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);
}
接着会走到 _nano_malloc_check_clear
2.3 _nano_malloc_check_clear
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;
// 在此处segregated_size_to_fit进行16字节对齐
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));
// 通过断点发现pMeta为0x0, ptr为NULL,会走到else里面
if (ptr) {
...
// 中间省略很多代码
} else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
这个时候我发现,size竟然变成了18
通过线程可以看到第一次进来的调用顺序
发现是调用了 _malloc_initialize_once
方法。这个先跳过,
然后继续往下走
会走到 ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
这个方法里面就是查找能开辟给定 slot_bytes
大小的内存的地方。直到查到返回。
static MALLOC_INLINE void *
segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
while (1) {
uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.
if (b < theLimit) { // Did we stay within the bound of the present slot allocation?
return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
} else {
if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
pMeta->slot_bump_addr = theLimit;
return 0; // We're toast
} else {
// One thread will grow the heap, others will see its been grown and retry allocation
_malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
// re-check state now that we've taken the lock
if (pMeta->slot_exhausted) {
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
return 0; // Toast
} else if (b < pMeta->slot_limit_addr) {
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
} else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
continue; // ... the slot has been successfully grown by us. Now try again.
} else {
pMeta->slot_exhausted = TRUE;
pMeta->slot_bump_addr = theLimit;
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
return 0;
}
}
}
}
}
那什么时候进行的16字节对齐的呢?
发现在查找地址之前有个方法 size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
这个方法返回后,slot_bytes就成了48了(16字节对齐过了)
2.4 segregated_size_to_fit -- 16字节对齐
16字节对齐方法
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
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;
}
通过计算发现,和上面讲的8字节对齐是不是道理一样,先给你补个差额(15),
然后通过右移4位,把 24 以下的二进制位干掉,
再左移4位,恢复原来的高二进制位的数据。
从而达到16字节对齐