對(duì)象是如何分配內(nèi)存的?對(duì)象是如何計(jì)算內(nèi)存大小的呢?對(duì)象內(nèi)存分配跟什么有關(guān)?
代碼分析
sizeof() 計(jì)算一個(gè)變量或者類型的大?。ㄒ宰止?jié)為單位)
class_getInstanceSize 計(jì)算對(duì)象所需要的內(nèi)存大小,結(jié)算結(jié)果遵循8字節(jié)對(duì)齊,其實(shí)現(xiàn)源碼如下:
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
malloc_size 計(jì)算對(duì)象的實(shí)際大小,使用時(shí)需要引入頭文件
//沒有添加任何屬性
@interface LNPerson : NSObject
@end
@implementation LNPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [LNPerson alloc];
NSLog(@"%lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
}
return 0;
}
打印結(jié)果:8 - 8 - 16
//添加一個(gè)屬性
@interface LNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LNPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LNPerson *person = [LNPerson alloc];
NSLog(@"打印結(jié)果: %lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
}
return 0;
}
打印結(jié)果: 8 - 16 - 16
//添加兩個(gè)屬性
@interface LNPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end
@implementation LNPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LNPerson *person = [LNPerson alloc];
NSLog(@"打印結(jié)果: %lu - %lu - %lu",sizeof(person),class_getInstanceSize([LNPerson class]),malloc_size((__bridge const void *)(person)));
}
return 0;
}
打印結(jié)果: 8 - 24 - 32
這里先附上一張數(shù)據(jù)類型占用內(nèi)存大小的表格:
圖1.1 數(shù)據(jù)類型占用內(nèi)存大小.png
由以上三分代碼運(yùn)行結(jié)果可以知道對(duì)象的內(nèi)存大小跟屬性有關(guān)(實(shí)際上是跟實(shí)例變量有關(guān))。代碼分析如下:
- 這里面sizeof的結(jié)果一直沒變化,實(shí)際上他只是計(jì)算person這個(gè)指針的大小,而這個(gè)指針是一直不變的,在OC里面對(duì)象指針的大小為8字節(jié)(其他數(shù)據(jù)類型占用內(nèi)存大小可以參考圖1-1)。
- 通過class_getInstanceSize打印結(jié)果可以看出隨著屬性的增多,開辟一個(gè)對(duì)象所需要的內(nèi)存也在增加(這里面需要注意的是屬性age是int屬性,如果按照對(duì)象所需要內(nèi)存大小計(jì)算應(yīng)該是8+8(name屬性大小)+ 4(age)= 20,可是為什么結(jié)果卻是24呢?實(shí)際上這是底層結(jié)構(gòu)體的內(nèi)存對(duì)齊遵循了8字節(jié)對(duì)齊的計(jì)算結(jié)果,關(guān)于結(jié)構(gòu)體內(nèi)存對(duì)齊可以參考相關(guān)資料。);
- malloc_size計(jì)算的是對(duì)象的實(shí)際內(nèi)存大小,我們看到,這跟class_getInstanceSize計(jì)算出來的大小不一致,這實(shí)際上是因?yàn)镺C對(duì)象內(nèi)存分配時(shí)遵循了16字節(jié)對(duì)齊原則,當(dāng)對(duì)象所需內(nèi)存小于16字節(jié)時(shí),還是會(huì)分配16字節(jié);當(dāng)對(duì)象所需內(nèi)存大于16字節(jié)且大小剛好為16的倍數(shù)時(shí),則按照所需大小分配;但是當(dāng)對(duì)象所需內(nèi)存大于16字節(jié)且大小不是16的倍數(shù)時(shí),則分配大于所需內(nèi)存大小的最小的16的倍數(shù)。
什么是內(nèi)存對(duì)齊
內(nèi)存對(duì)齊”應(yīng)該是編譯器的“管轄范圍”。編譯器為程序中的每個(gè)“數(shù)據(jù)單元”安排在適當(dāng)?shù)奈恢蒙?。?nèi)存對(duì)齊意味將數(shù)據(jù)類型寫入到內(nèi)存地址時(shí)是按照它們大小切割的。簡(jiǎn)單說就是如果內(nèi)存地址是n字節(jié)的倍數(shù),那么我們說這n字節(jié)是內(nèi)存對(duì)齊的,注意,這里n是2的冪,說白了,內(nèi)存地址正好放下n字節(jié)的倍數(shù),兩者相除余數(shù)為零,正好整除。例如,從上面的代碼分析可以看出對(duì)象person所需要的實(shí)際大小為24字節(jié),而實(shí)際大小確是32字節(jié),這是因?yàn)镺C對(duì)象的內(nèi)存是16字節(jié)對(duì)齊的。
為什么需要內(nèi)存對(duì)齊
那編譯器為什么要進(jìn)行內(nèi)存對(duì)齊呢?主要基于以下兩個(gè)原因:
- 1、平臺(tái)原因(移植原因):
不是所有的硬件平臺(tái)都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。 - 2、性能原因
數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于,為了訪問未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對(duì)齊的內(nèi)存訪問僅需要一次訪問。
內(nèi)存對(duì)齊遵循什么樣的原則
1、數(shù)據(jù)成員對(duì)齊規(guī)則:結(jié)構(gòu)(struct)(或聯(lián)合(union))的數(shù)據(jù)成員,第一個(gè)數(shù)據(jù)成員放在offset為0的地方,以后每個(gè)數(shù)據(jù)成員存儲(chǔ)的起始位置要從該成員大小或者成員的子成員大?。ㄖ灰蓡T有子成員,比如說數(shù)組,結(jié)構(gòu)體等)的整數(shù)倍開始(比如int為4字節(jié),則要從4的整數(shù)倍地址開始存儲(chǔ)。min(當(dāng)前開始的位置m n))m = 9, n= 4:9 10 11 12 ,12是4的整數(shù)倍,所以從12開始存儲(chǔ)
2、結(jié)構(gòu)體作為成員:如果一個(gè)結(jié)構(gòu)體里有些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素的整數(shù)倍地址開始存儲(chǔ)。(struct a里面存儲(chǔ)struct b, b里有char、int、 double等元素,那b應(yīng)該從8的整數(shù)倍開始存儲(chǔ)。)
3、收尾工作:結(jié)構(gòu)體的總大小,也就是sizeof的結(jié)果,必須是其內(nèi)部最大成員的整數(shù)倍,不足的要補(bǔ)齊。
舉個(gè)例子:
// 64位
struct MyStruct1 {
double a;//8字節(jié) a存儲(chǔ)為0~7位置共8位
char b;//1字節(jié) 8是1的整數(shù)倍,所以b從8位置開始存儲(chǔ),共1位
int c;// 4字節(jié) 前8位已被占用,9不是4的倍數(shù),所以得往前找最近的4的倍數(shù)12,從12位開始存儲(chǔ),共四位12~15
short d;//2字節(jié) 16位剛好是2的倍數(shù),從16位開始存儲(chǔ),共兩位16~17,可以推出struct1的所需大小為0~17共18位
}struct1;// 但是結(jié)構(gòu)大小是其內(nèi)部最大成員(這里的a,8字節(jié))的整數(shù)倍,因此最小為24
struct MyStruct2 {// 同上面的推理
double a;//8 0~7
int c;// 4 8是4的倍數(shù) 8~11
char b;//1 12
short d;//2 13不是2的整數(shù)倍, 故14~15,所需大小位0~15共16位,實(shí)際大小應(yīng)該為16
}struct2;
struct MyStruct3 {// 同上面的推理
double a;//8 0~7
int c;// 4 8是4的倍數(shù) 8~11
char b;//1 12
short d;//2 13不是2的整數(shù)倍, 故14~15,所需大小15
//如果一個(gè)結(jié)構(gòu)體里有些結(jié)構(gòu)體成員,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素的整數(shù)倍地址開始存儲(chǔ)。所以雖然這里myStruct2的大小為16字節(jié),但是依然以8字節(jié)為倍數(shù)開始計(jì)算,這里16剛好是8的整數(shù)倍,所以myStruct2從16位開始,大小16位,所以應(yīng)該是16~31位,總共大小32位
struct MyStruct2 myStruct2;//16
}struct3;// 32
打印這三個(gè)結(jié)構(gòu)體:
NSLog(@"struct1大?。?ld",sizeof(struct1));
NSLog(@"struct2大?。?ld",sizeof(struct2));
NSLog(@"struct3大?。?ld",sizeof(struct3));
打印結(jié)果:
2021-07-25 17:12:57.383194+0800 MemoryAlignmentTest[8346:636413] struct1大?。?4
2021-07-25 17:12:57.383265+0800 MemoryAlignmentTest[8346:636413] struct2大?。?6
2021-07-25 17:12:57.383295+0800 MemoryAlignmentTest[8346:636413] struct3大小:32
結(jié)果是符合規(guī)則的。
備注:OC對(duì)象的底層結(jié)構(gòu)是結(jié)構(gòu)體,結(jié)構(gòu)體大小內(nèi)存分配遵循8字節(jié)對(duì)齊原則。但是OC對(duì)象在結(jié)構(gòu)8字節(jié)對(duì)齊計(jì)算出的內(nèi)存大小的基礎(chǔ)上遵循16字節(jié)對(duì)齊原則。這么做應(yīng)該是出于兼容性、容錯(cuò)的等方面的考慮,給對(duì)象留點(diǎn)空間,避免擠滿。
