一、結(jié)構(gòu)體內(nèi)存對(duì)齊
1.1 結(jié)構(gòu)體內(nèi)存對(duì)齊三大原則
數(shù)據(jù)成員對(duì)?規(guī)則
結(jié)構(gòu)體(struct)或聯(lián)合體(union)的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員,比如說是數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始。(比如int為4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ)。)
min(當(dāng)前開始的位置m n) m=9 n=4 9 10 11 12結(jié)構(gòu)體作為成員
如果一個(gè)結(jié)構(gòu)里有某些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(chǔ)。
比如struct a中存有struct b,b中char,int,double等元素。那b應(yīng)該從8的整數(shù)倍開始存儲(chǔ)。
struct structB {
char c;
int i;
double d;
};
struct structA {
char c;//1
int i;//4
struct structB b;//從8開始存儲(chǔ)
};
-
補(bǔ)齊
結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍。不足的要補(bǔ)?。
1.2 基本數(shù)據(jù)類型內(nèi)存大小
| C | OC | 32位 | 64位 |
|---|---|---|---|
| bool | BOOL(64位) | 1 | 1 |
| signed char | (__signed char)int8_t、BOOL(32位) | 1 | 1 |
| unsigned char | Boolean | 1 | 1 |
| short | int16_t | 2 | 2 |
| unsigned short | unichar | 2 | 2 |
| int 、int32_t | NSInteger(32位)、boolean_t(32位) | 4 | 4 |
| unsigned int | boolean_t(64位)、NSUInteger(32位) | 4 | 4 |
| long | NSInteger(64位) | 4 | 8 |
| unsigned long | NSUInteger(64位) | 4 | 8 |
| long long | int64_t | 8 | 8 |
| float | CGFloat(32位) | 4 | 4 |
| double | CGFloat(64位) | 8 | 8 |
1.3 獲取內(nèi)存大小
1.3.1 sizeof(expression-or-type)
sizeof()是C/C++中的關(guān)鍵字,它是一個(gè)運(yùn)算符,不是函數(shù)。作用是取得一個(gè)對(duì)象(數(shù)據(jù)類型或者數(shù)據(jù)對(duì)象)的長(zhǎng)度(即占用內(nèi)存的大小,以byte為單位,返回size_t)?;緮?shù)據(jù)類型(int、double等)的大小與系統(tǒng)相關(guān)。結(jié)構(gòu)體涉及字節(jié)對(duì)齊。
示例:
struct Stu {
char c;
int i;
double d;
};
void test() {
//基本數(shù)據(jù)類型
int age = 18;
size_t sizeAge1 = sizeof(age);
size_t sizeAge2 = sizeof age;
size_t sizeAge3 = sizeof(int);
NSLog(@"age size: %zu, %zu, %zu",sizeAge1,sizeAge2,sizeAge3);
//結(jié)構(gòu)體
struct Stu s;
s.c = 'c';
s.i = 18;
s.d = 180.0;
size_t sizeS1 = sizeof(s);
size_t sizeS2 = sizeof s;
size_t sizeS3 = sizeof(struct Stu);
NSLog(@"s size: %zu, %zu, %zu",sizeS1,sizeS2,sizeS3);
//指針
NSObject *obj = [NSObject alloc];
size_t sizeObj1 = sizeof(obj);
size_t sizeObj2 = sizeof obj;
size_t sizeObj3 = sizeof(NSObject *);
NSLog(@"obj size: %zu, %zu, %zu",sizeObj1,sizeObj2,sizeObj3);
}
輸出:
age size: 4, 4, 4
s size: 16, 16, 16
obj size: 8, 8, 8
- 通過類型和實(shí)例都可以獲取內(nèi)存大小,這也說明開辟的內(nèi)存大小在類型確定后就已經(jīng)確定了。
-
sizeof是運(yùn)算符不是函數(shù)。3種語法形式都可以,需要注意的是通過類型獲取的方式必須在()中。
1.3.2 class_getInstanceSize
這個(gè)函數(shù)是runtime提供的獲取類的實(shí)例所占用的內(nèi)存大小。大小只與成員變量有關(guān)。獲取的是實(shí)際占用的空間(8字節(jié)對(duì)齊)。
源碼如下:
#ifdef __LP64__
# define WORD_MASK 7UL
#else
# define WORD_MASK 3UL
#endif
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
這里就和alloc中instanceSize沒有命中緩存的邏輯一致了。
1.3.3 malloc_size
malloc_size就是alloc中實(shí)際開辟的空間。
1.4 結(jié)構(gòu)體對(duì)齊案例
1. 有如下結(jié)構(gòu)體Struct1和Struct2分別占用多大內(nèi)存?
struct Struct1 {
double a; // [0,7]
char b; // [8]
int c; // 根據(jù)第一準(zhǔn)則要從4的倍數(shù)開始,所以[12,13,14,15]。跳過9,10,11
short d; //[16,17]
}struct1;
//根據(jù)第三準(zhǔn)則總大小要是8的倍數(shù),那就要分配24字節(jié)。
struct Struct2 {
double a; //[0,7]
int b; //[8,11]
char c; //[12]
short d; //根據(jù)準(zhǔn)則1跳過13,從14開始 [14,15]
}struct2;
//這里0~15大小本來就為16了,所以不需要補(bǔ)齊了。
驗(yàn)證:
NSLog(@"struct1 size :%zu\nstruct2 size:%zu",sizeof(struct1),sizeof(struct2));
輸出:
struct1 size :24
struct2 size:16
- 結(jié)構(gòu)體中數(shù)據(jù)類型順序不一致占用的內(nèi)存大小可能不一致。
- 大小計(jì)算從
0開始,Struct2并沒有進(jìn)行第三原則補(bǔ)齊。
2. 增加一個(gè)Struct3中有結(jié)構(gòu)體嵌套,那么占用大小是多少?
struct Struct3 {
double a; //[0,7]
int b; //[8,11]
char c; //[12]
short d; //跳過13 [14,15]
int e; // [16,19]
struct Struct1 str; //根據(jù)準(zhǔn)則2,Struct1最大元素為`double`類型,所以從24開始。根據(jù)`Struct1`分配的時(shí)候24個(gè)字節(jié),所以str為[24,47]
}struct3;
//所以Struct3占用內(nèi)存大小為48字節(jié)。
驗(yàn)證:
NSLog(@"struct3 size :%zu",sizeof(struct3));
struct3 size :48
在這里可能有個(gè)疑問準(zhǔn)則3是先作用在Struct1再作用在Struct3還是最后直接作用在Struct3?不防驗(yàn)證一下:
struct Struct4 {
struct Struct1 str;
char c;
}struct4;
Struct1本身占用18字節(jié),補(bǔ)齊后占用24字節(jié)。如果Struct4最終占用32字節(jié)那么就是第一種情況,占用24字節(jié)則是第二種情況。
NSLog(@"struct4 size :%zu",sizeof(struct4));
struct4 size :32
這也就驗(yàn)證了猜想,結(jié)構(gòu)體嵌套從內(nèi)部開始補(bǔ)齊。這也符合常理。
3. 修改結(jié)構(gòu)體如下:
struct S1 {
int a; // 4 [0,3]
char b;// 1 [4]
short c; // 2 [6,7]
}; // 0~7 8字節(jié)
struct S2 {
double a; // 8 [0,7]
char b; // 1 [8]
struct S1 s1; // 8 [12,19] 按s1自身中存的最大a的4字節(jié)的倍數(shù)對(duì)齊
bool c; // 1 [20]
};
//0~20一共21個(gè)字節(jié),按最大的8字節(jié)對(duì)齊。應(yīng)該24字節(jié)。
struct S2 s2;
NSLog(@"size :%zu",sizeof(s2));
這個(gè)時(shí)候s2大小為多少?
分析:
- S1:
-
int a占4個(gè)字節(jié),從[0~3]。 -
char b占1個(gè)字節(jié),[4]。 -
short c占2個(gè)字節(jié),需要以2字節(jié)對(duì)齊,所以跳過5[6~7] -
S1整體從0~7不需要補(bǔ)齊。占8字節(jié)。
-
- S2:
-
double a占8字節(jié),[0~7]。 -
char b占1字節(jié),[8]。 -
struct S1 s1占8字節(jié)。由于S1內(nèi)部最大元素為int a所以需要4倍對(duì)齊,所以[12~19]。 -
bool c占1字節(jié),[20]。 -
S2整體從0~21一共21字節(jié),需要按S2中最大元素double a補(bǔ)齊。所以應(yīng)該是24字節(jié)。
-
輸出:
size :24
1.5 對(duì)齊原理分析
為什么要根據(jù)數(shù)據(jù)類型跳過部分內(nèi)存呢?跳過的部分為什么不能存儲(chǔ)數(shù)據(jù)?

對(duì)于不優(yōu)化聯(lián)系存儲(chǔ)的情況,CPU讀取8~15的內(nèi)存數(shù)據(jù),需要先讀取1字節(jié)再讀取4字節(jié),cpu對(duì)于要讀取的數(shù)據(jù)大小是有變化的。而優(yōu)化后cpu先讀取4字節(jié)(由于白色3字節(jié)空白所以可以直接讀取4字節(jié))再讀取4字節(jié)在這段內(nèi)存中是沒有變化的。相比于第一種優(yōu)化后cpu的要進(jìn)行的操作少了,這就實(shí)現(xiàn)了通過空間換取時(shí)間。
二、系統(tǒng)內(nèi)存開辟
2.1 案例
HPObject定義如下:
@interface HPObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end
調(diào)用:
#import "HPObject.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
HPObject *hpObj = [HPObject alloc];
hpObj.name = @"HotpotCat";
hpObj.age = 18;
NSLog(@"sizeof:%zu class_getInstanceSize:%zu malloc:%zu",sizeof(hpObj),class_getInstanceSize([HPObject class]),malloc_size((__bridge const void *)(hpObj)));
那么sizeof、class_getInstanceSize、malloc_size分別輸出多少呢?
驗(yàn)證:
sizeof:8 class_getInstanceSize:40 malloc:48
-
hpObj是一個(gè)結(jié)構(gòu)體指針sizeof返回8。 -
class_getInstanceSize由于存在isa和8字節(jié)對(duì)齊所以返回40。 -
malloc_size為什么返回48呢?
2.2 malloc_size分析
查看API發(fā)現(xiàn)malloc_size在usr/include中:

那么就需要源碼查看
calloc的實(shí)現(xiàn)邏輯了。calloc源碼在libmalloc中。在源碼調(diào)試中實(shí)現(xiàn)如下:
void *p = calloc(1, 40);
NSLog(@"%zu",malloc_size(p));
calloc
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
calloc調(diào)用了_malloc_zone_calloc
_malloc_zone_calloc核心實(shí)現(xiàn)如下
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;
//……
ptr = zone->calloc(zone, num_items, size);
//……
return ptr;
}
內(nèi)部調(diào)用了zone->calloc。但是只有calloc的聲明沒有實(shí)現(xiàn)。這個(gè)時(shí)候有兩種方式去找到調(diào)用:
-
control + step into跟進(jìn)去看跳轉(zhuǎn)到了哪里。 - 直接
po/pzone->calloc查看(有賦值就有存儲(chǔ)值)
(lldb) po zone->calloc
(.dylib`default_zone_calloc at malloc.c:385)
(lldb) p zone->calloc
(void *(*)(_malloc_zone_t *, size_t, size_t)) $0 = 0x00000001002e1bb7 (.dylib`default_zone_calloc at malloc.c:385)
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->calloc調(diào)用到了nano_calloc
nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
//NULL邏輯不用看
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
if (total_bytes <= NANO_MAX_SIZE) {
//重點(diǎn)邏輯
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
//help邏輯大概出錯(cuò)的時(shí)候才需要
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
得到核心邏輯在_nano_malloc_check_clear中。
_nano_malloc_check_clear

_nano_malloc_check_clear中計(jì)算大小的邏輯是在segregated_size_to_fit中完成的。
segregated_size_to_fit
#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) {
//當(dāng)size為0的時(shí)候直接返回16
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
//(40 + 16 - 1) >> 4 << 4 16字節(jié)對(duì)齊
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;
}
16字節(jié)對(duì)齊,向上對(duì)齊。這也就是在系統(tǒng)的內(nèi)存堆區(qū)中對(duì)象的內(nèi)存是16字節(jié)對(duì)齊,成員變量是以8字節(jié)對(duì)齊(結(jié)構(gòu)體內(nèi)部)。對(duì)象與對(duì)象是16字節(jié)對(duì)齊。

2.3 為什么以16字節(jié)對(duì)齊?
為什么對(duì)象不以8字節(jié)對(duì)齊?而以16字節(jié)對(duì)齊?

假如一個(gè)對(duì)象內(nèi)部成員變量都是
8字節(jié)大小。
- 以
8字節(jié)對(duì)齊內(nèi)部沒有多余空間,更容易發(fā)生訪問錯(cuò)誤。 - 以
16字節(jié)對(duì)齊內(nèi)部有多余空間,不容易發(fā)生訪問錯(cuò)誤。
對(duì)于64字節(jié)的空間:
16 32 48 64
8 16 24 32 40 48 56 64
- 明顯以
16字節(jié)對(duì)齊訪問對(duì)象和成員變量碰到一起的概率小了。16字節(jié)對(duì)齊4次,8字節(jié)對(duì)齊8次。 - 任何對(duì)象都繼承自
NSObject,但是很少有對(duì)象只有一個(gè)isa。所以最小的對(duì)象都應(yīng)該是16。 - 如果用
32字節(jié)對(duì)齊呢?很明顯空間浪費(fèi)太大。
三、getInstanceSize與calloc
getInstanceSize的邏輯可以查看上一篇文章alloc流程。getInstanceSize與calloc中字節(jié)對(duì)齊關(guān)系如下:

-
instanceSize正常情況下會(huì)走fastInstanceSize分支進(jìn)行16字節(jié)對(duì)齊。(setFastInstanceSize在_read_images的時(shí)候就完成了進(jìn)行了8字節(jié)對(duì)齊邏輯,但是沒有進(jìn)行最小16字節(jié)兜底)。 -
instanceSizealignedInstanceSize分支進(jìn)行了8字節(jié)對(duì)齊并且有最小16字節(jié)兜底。但是沒有進(jìn)行16字節(jié)對(duì)齊。 -
calloc中進(jìn)行了16字節(jié)對(duì)齊和最小16字節(jié)的修正。相當(dāng)于是對(duì)instanceSize的兩個(gè)分支的兜底。
成員變量字節(jié)對(duì)齊是8字節(jié)對(duì)齊,對(duì)象的內(nèi)存對(duì)齊是16字節(jié)對(duì)齊
總結(jié)
- 結(jié)構(gòu)體對(duì)齊(三個(gè)原則)
-
3個(gè)原則- 數(shù)據(jù)成員對(duì)?規(guī)則:從成員大小或者成員的子成員大小整數(shù)倍開始。
- 結(jié)構(gòu)體作為成員:從內(nèi)部成員最大元素大小的整數(shù)倍地址開始存儲(chǔ)。
- 補(bǔ)齊:必須是內(nèi)部最大成員的整數(shù)倍,不足的要補(bǔ)?。
- 對(duì)齊原理:優(yōu)化
CPU讀取速度,以空間換時(shí)間。 - 結(jié)構(gòu)體嵌套補(bǔ)齊從內(nèi)部開始補(bǔ)齊。
-
- 內(nèi)存大小獲取
-
sizeof:是運(yùn)算符,不是函數(shù)。獲取對(duì)象的長(zhǎng)度(對(duì)象本身)。 -
class_getInstanceSize:獲取類的實(shí)例所占用的內(nèi)存大小。大小只與成員變量有關(guān)。 -
malloc_size:alloc中實(shí)際開辟的空間。
-
-
calloc16字節(jié)對(duì)齊,最小返回16。- 最終分配的內(nèi)存大小邏輯在
segregated_size_to_fit中。以16字節(jié)對(duì)齊向上取整 - 為什么以
16字節(jié)對(duì)齊?減少訪問錯(cuò)誤。
- 最終分配的內(nèi)存大小邏輯在