之前通過 objc
的源碼探索了 alloc
的內(nèi)部流程,到最后會調(diào)用 size = cls->instanceSize(extraBytes);
方法脆霎,獲取內(nèi)存大小,但是這個大小到底是怎么計算的呢?
獲取大小后框往,會調(diào)用 calloc(1, size)
方法開辟內(nèi)存大小,開辟的時候又有什么不同呢知允?
這次就繼續(xù)探索一下系統(tǒng)的內(nèi)存分配撒蟀。
一、屬性所占內(nèi)存計算
從應(yīng)用代碼開始
@interface GLPerson : NSObject
// 會有一個隱藏屬性 isa 占8個字節(jié)
@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流程中簡單說過温鸽,屬性內(nèi)存分配的時候是8字節(jié)對齊保屯,GLPerson類的實例所占大小計算為 8 (isa) + 8 (name) + 4 (height) + 1 (char1) + 1 (char1) == 22
。(模擬計算結(jié)果涤垫,實際在跟源碼的時候蘋果有內(nèi)存優(yōu)化配椭,會把 4 (height) + 1 (char1) + 1 (char1)
放到一個8字節(jié)里面存儲,這樣避免了浪費)
然后會對22進(jìn)行8字節(jié)對齊雹姊,得到的是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;
}
根據(jù)上面4個方法的調(diào)用順序
-
class_getInstanceSize
: 內(nèi)部調(diào)用alignedInstanceSize
返回 -
alignedInstanceSize
: 內(nèi)部調(diào)用word_align(unalignedInstanceSize())
返回 -
unalignedInstanceSize
: 內(nèi)部調(diào)用data()->ro()->instanceSize
返回,data()
和ro()
的數(shù)據(jù)會在loadImage
的時候完成吱雏,instanceSize會根據(jù)類有多少屬性敦姻,返回已經(jīng)經(jīng)過編譯器優(yōu)化存儲后的結(jié)果 -
word_align
: 這個會對實例大小做8字節(jié)對齊,會返回8的倍數(shù)大小
1.1 對齊計算
word_align
就是8字節(jié)對齊計算
# 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 計算結(jié)構(gòu)體大小
咱們知道OC繼承與C镰惦,并且 struct objc_class : objc_object
, objc_class
的源碼也是一個結(jié)構(gòu)體犬绒,結(jié)構(gòu)體在計算大小的時候有3個原則:
1旺入、每個成員的偏移量都必須是當(dāng)前成員所占內(nèi)存大小的整數(shù)倍如果不是編譯器會在成員之間加上填充字節(jié)。
2凯力、結(jié)構(gòu)體作為成員: 如果?個結(jié)構(gòu)?有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最?元素??的整數(shù)倍地址開始存儲茵瘾。(struct a?存有struct b,b?有char,int ,double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲)
3、當(dāng)所有成員大小計算完畢后咐鹤,編譯器判斷當(dāng)前結(jié)構(gòu)體大小是否是結(jié)構(gòu)體中最寬的成員變量大小的整數(shù)倍 如果不是會在最后一個成員后做字節(jié)填充拗秘。
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
(根據(jù)原則1,第13位不是2的整數(shù)倍祈惶,往后移雕旨,14滿足) 14-->15
;
實際總共:0--> 15
為 16,
再根據(jù)原則3捧请,需要是8的倍數(shù)凡涩,16滿足,最后就是16疹蛉。
structB:
age 0 --> 3
;
height
(根據(jù)原則1活箕,4不滿足8的整數(shù)倍,往后移氧吐,8滿足) 8-->15
;
char1 16
;
short1
(第17位不是2的整數(shù)倍讹蘑,往后移末盔,18滿足)18-->19
;
實際總共:0--> 19
為 20,
再根據(jù)原則3座慰,需要是8的倍數(shù)陨舱,所以最后是24。
structC:
age 0 --> 3
;
structB sb
(根據(jù)原則2版仔,因為structB里面最大的是8字節(jié)游盲,4不滿足8的整數(shù)倍,往后移蛮粮,8滿足益缎,可知structB占24) 8-->31
;
sex 32
;
實際總共:0--> 32
為 33,
再根據(jù)原則3然想,需要是8的倍數(shù)莺奔,所以最后是40。
1.3 編譯優(yōu)化
測試如下代碼
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
// 默認(rèn)屬性isa 占8個字節(jié)
@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
通過輸出結(jié)果可以知道变泄,structB大小為40令哟,LGPerson真正占用大小為24。為什么妨蛹,這里面就是系統(tǒng)底層做了編譯優(yōu)化屏富,在字節(jié)對齊的基礎(chǔ)上,又節(jié)省了空間蛙卤『莅耄可以通過打印內(nèi)存地址查看
可以使用x 或者 memory read命令查看某個對象的內(nèi)存情況。
更方便的查看4xg規(guī)則
可以通過po命令打印出內(nèi)存中對應(yīng)的值如下:
可以知道底層把 height
char1
char2
char3
char4
放到了一個8字節(jié)里面颤难。
這樣不會像結(jié)構(gòu)體那樣按順序存儲神年,中間會有很多補位。在保證對齊原則的情況下乐严,極大的節(jié)省了內(nèi)存空間瘤袖。
二衣摩、calloc() 源碼分析
分析 malloc
的源碼昂验,官方地址,本次分析的是libmalloc-283.100.6
版本艾扮。
void *p = calloc(1, 40);
NSLog(@"%lu",malloc_size(p));
console: 48
為什么開辟了48既琴??泡嘴?從源碼里面找下答案
分析 malloc
源碼的時候甫恩,還是需要配置一個能編譯運行的源碼工程的,這樣斷點能走進(jìn)去酌予,方便分析磺箕。
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)
發(fā)現(xiàn) calloc
又調(diào)回去了奖慌??松靡?只看源碼的話简僧,確實找不到下一步走向了哪里。
還是需要編譯運行工程雕欺,通過調(diào)用 void *p = calloc(1, 40);
在 ptr = zone->calloc(zone, num_items, size)
打個斷點岛马。然后打印下,看看調(diào)用方法
也可以通過按住 control
點擊 step into
多次屠列,進(jìn)入下一個方法調(diào)用啦逆。
可知下一步來到了 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進(jìn)行16字節(jié)對齊
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));
// 通過斷點發(fā)現(xiàn)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;
}
這個時候我發(fā)現(xiàn),size竟然變成了18
通過線程可以看到第一次進(jìn)來的調(diào)用順序
發(fā)現(xiàn)是調(diào)用了 _malloc_initialize_once
方法苛让。這個先跳過盲镶,
然后繼續(xù)往下走
會走到 ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
這個方法里面就是查找能開辟給定 slot_bytes
大小的內(nèi)存的地方。直到查到返回蝌诡。
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;
}
}
}
}
}
那什么時候進(jìn)行的16字節(jié)對齊的呢溉贿?
發(fā)現(xiàn)在查找地址之前有個方法 size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
這個方法返回后,slot_bytes就成了48了(16字節(jié)對齊過了)
2.4 segregated_size_to_fit -- 16字節(jié)對齊
16字節(jié)對齊方法
#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;
}
通過計算發(fā)現(xiàn)浦旱,和上面講的8字節(jié)對齊是不是道理一樣宇色,先給你補個差額(15),
然后通過右移4位颁湖,把 24 以下的二進(jìn)制位干掉宣蠕,
再左移4位,恢復(fù)原來的高二進(jìn)制位的數(shù)據(jù)甥捺。
從而達(dá)到16字節(jié)對齊