? 这是我参与更文挑战的第1天,活动详情查看: 更文挑战
系列文章:
iOS底层探险① OC对象初始化流程
iOS底层探险② 从LLVM源码分析为什么alloc、retain、isKindOfClass等AWZ、RR、CORE系列方法没有走自身的IMP
iOS底层探险③ iOS malloc内存分配底层原理
…
⭐ 表示「核心逻辑内容」
? 表示「分支知识扩展」
表示「苹果官方文档或者源码」
本文用到的资源:
objc4 源码
libmalloc 源码
malloc内存分配底层原理
源码分析
在 objc4源码 里可以看到 alloc 、 alloc init 、 new 最终调用在执行 _class_createInstanceFromZone 函数,这个函数向系统申请内存的功能调用的是 obj = (id)calloc(1, size);
而 calloc 函数的实现是在 libmalloc源码里
calloc 函数具体做了什么呢,先看 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)
{
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);
... 省略非必要代码
return ptr;
}
复制代码
⭐ 核心代码: ptr = zone->calloc(zone, num_items, size);
怎么又是一个 calloc ??这不是死循环了么、、、(O_O)? 其实不是,这里的 calloc 是 zone 这个结构体里的函数,而最开始的 calloc 是原始的 c 函数,是两个函数实现, 接下来查看 zone 的逻辑,由于上一步传过来的是 全局变量 default_zone
:
static malloc_zone_t *default_zone = &virtual_default_zone.malloc_zone;
复制代码
可以看到 default_zone 是 virtual_default_zone.malloc_zone
的一个引用,是个 malloc_zone_t 类型 virtual_default_zone 的源码:
typedef struct {
malloc_zone_t malloc_zone;
uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)];
} virtual_default_zone_t;
static virtual_default_zone_t virtual_default_zone
__attribute__((section("__DATA,__v_zone")))
__attribute__((aligned(PAGE_MAX_SIZE))) = {
NULL,
NULL,
default_zone_size,
default_zone_malloc,
default_zone_calloc,
default_zone_valloc,
default_zone_free,
default_zone_realloc,
default_zone_destroy,
... 省略非必要代码
};
复制代码
malloc_zone_t
结构体的定义源码如下:
typedef struct _malloc_zone_t {
/* Only zone implementors should depend on the layout of this structure;
Regular callers should use the access functions below */
void *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */
void *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */
size_t (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
void *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
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 */
void *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
void (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
void *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone); /* zone is destroyed and all memory reclaimed */
const char *zone_name;
... 省略非必要代码
boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);
} malloc_zone_t;
复制代码
继续之前的代码,打开反汇编调试可以看到接下来走了 default_zone_calloc
函数
不通过汇编,通过断点调试也可以查看, 断好之后 p zone->calloc
也可以看到
接下来查看 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);
}
复制代码
⭐ 可以看到 zone = runtime_default_zone();
形参 zone 被换成了 runtime_default_zone()
同上汇编查看接下来调用的是 nano_zone
的 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_MAX_SIZE 就调用 void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
初始化 p
返回, 超过 NANO_MAX_SIZE 就是用 helper_zone
来创建大对象 (用的是 scalable_zone
)
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
复制代码
可以看到 malloc 底层小对象、大对象内存分配的临界值升级 256字节 ,接下来查看 _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;
/// ------------ slot_bytes ----
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)
... 省略非必要代码
} 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;
}
复制代码
核心逻辑执行 segregated_next_block
函数,而传入的内存大小参数是 slot_bytes
, 接下来查看 slot_bytes
的初始化方法:
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 NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
#define SHIFT_NANO_QUANTUM 4
复制代码
⭐ 可以看到核心逻辑是按照 NANO_REGIME_QUANTA_SIZE 16
对齐,算法是 原始Size+15 进位后 尾部四位抹零 举?:
接下来查看 segregated_next_block
:
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;
}
}
}
}
}
复制代码
⭐ 最终得到内存的方法是调用系统内核 OSAtomicAdd64Barrier
原子的为 pMeta->slot_bump_addr 添加 slot_bytes(16字节对齐)的长度,偏移到 下一个地址
然后减去 slot_bytes 得到 申请好的内存地址的开始地址 返回。
流程图
上述源码分析,执行顺序简单总结一下: calloc(1,size)
-> _malloc_zone_calloc
-> ptr = zone->calloc(zone, num_items, size);
-> nano_calloc
-> _nano_malloc_check_clear
-> segregated_next_block
-> OSAtomicAdd64Barrier
流程图如下:
补充一 结构体内存大小规则
分析CPU是64位架构
规则 1、成员开始位置
- 成员开始位置,必须是自身大小或最大成员大小的整数倍
- 基础数据类型用自己身的类型大小(如:Int 4字节)的整数倍
- 复合类型(数组、结构体等)用自身最大的成员变量大小的整数倍
规则 2、结构内存对齐
- 在上述规则累加后补充: 结构体的最终大小必须是最大成员大小的整数倍
比如计算大小后,算出来是12字节,最大成员8字节 ,最终是16字节
规则 3、最后 malloc 内存对齐
- 最终结构体大小会根据操作系统做内存对齐,iOS 64位CPU是16字节对齐,在上述
segregated_size_to_fit
已经做详细说明,不再赘述
举?
struct Struct_1 {
double a; // 8 bit
char b; // 1 bit
double c; // 8 bit
short d; // 2 bit
};
struct Struct_2 {
double a; // 8 bit
char b; // 1 bit
short d; // 2 bit
double c; // 8 bit
};
复制代码
分析 Struct_1
步骤1: a 8字节 ———— 当前大小8字节
步骤2: b 1字节 —— 满足1的整数倍 — 当前大小9字节
步骤3: c 8字节 —— 不满足8的整数倍,向后调整到 16开始存储8字节 — 当前大小24字节
步骤4: d 2字节 —— 满足2的整数倍 — 当前大小26字节
步骤5: 结构体内存对齐, 最大成员是 8字节 26字节对齐后当前大小32字节
步骤6: malloc内存对齐,最后是32字节 是内存16字节对齐的整数倍: 最终32字节
分析 Struct_2
步骤1: a 8字节 ———— 当前大小8字节
步骤2: b 1字节 —— 满足1的整数倍 — 当前大小9字节
步骤3: d 2字节 —— 不满足2的整数倍 — 10字节开始+2字节, 当前大小12字节
步骤4: c 8字节 —— 不满足8的整数倍,向后调整到 16开始存储8字节 — 当前大小24字节
步骤5: 结构体内存对齐, 最大成员是 8字节,24字节满足8的备注
步骤6: malloc内存对齐,32字节 是内存16字节对齐的整数倍
结果: 32字节
可以看到上述两个结构体,成员类型是一样的,只是位置调整sizeof()
得出的结构体大小就不一样了,第一个 32字节 ,第二个 24字节 ,但是最终 malloc 内存对齐的话都是 32字节。
拓展 C++ 结构体虚表
? 上述结构体 Struct_1
结构体大小是 32字节 ,假如继承一个有个 虚函数 的结构体呢
struct Struct_0 {
// virtual pointer //8 bit
BOOL v1; //1 bit
virtual void foo(){
}
}struct0;
struct Struct_1 : Struct_0 {
// virtual pointer //8 bit
double a; // 8 bit
char b; // 1 bit
double c; // 8 bit
short d; // 2 bit
}struct1;
复制代码
在父结构体 Struct_0
有一个 BOOL 变量和一个虚函数,Struct_1
继承它,有重名 b
, 计算后 Struct_0
结构体大小是 16字节,内存对齐后还是 16字节。 Struct_1
结构体大小是 48字节,内存对齐后还是 48字节。
这是因为,父类有虚函数的原因,有虚函数的话结构体头部会存放一个指向该类、结构体(C++类和结构体只是默认权限不一样)虚表的指针,64位下占8字节。 值得一提的是,一个结构体的不同实例的虚指针指向的是一个同虚表地址。 子结构体成员变量 b 虽然和父类重名但是不影响,也会存储。
补充二 scalable_zone相关
scalable_zone
是分配iOS 大对象使用的 zone,利用这一特性,在项目实际开发中也又很有用的作用, hook的话可以监控大内存对象的创建,对线上的 OOM(out of memory) Crash
定位分析会有很大帮助, 关于 OOM
底层原理在后续的文章会更新
写在最后
文章的代码环境基于苹果 objc4-818.2 、libmalloc-317 源码Xcode12,内容尽量做到了结构化、精炼,以便节省读者阅读时间成本,如有哪里书写不对或者补充欢迎及时交流沟通~~
最后,感谢您的阅读 一切祝好哈 have a nice codding \(^o^)/~