问题先行:
1、int、bool、float、double、string、指针地址、占用的内存大小是多少?
2、一个无任何属性对象系统开辟内存大小是?
3、一个无任何属性对象实际占用的内存大小是?
4、成员变量为什么是8字节对齐,对象却是16字节对齐?
资料准备:
1、objc源码下载opensource.apple.com/tarballs/ob…
2、libmalloc源码下载opensource.apple.com/tarballs/li…
字节对齐
前情提要:
很多 CPU(如基于 Alpha,IA-64,MIPS,和 SuperH 体系的)拒绝读取未对齐数据。当一个程序要求这些 CPU 读取未对齐数据时,这时 CPU 会进入异常处理状态并且通知程序不能继续执行。所以,如果编译器不进行内存对齐,那在很多平台的上的开发将难以进行。
CPU 不是以字节为单位存取数据的。CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。每次内存存取都会产生一个固定的开销,减少内存存取次数将提升程序的性能。所以 CPU 一般会以 2/4/8/16/32 字节为单位来进行存取操作。我们将上述这些存取单位也就是块大小称为(memory access granularity)内存存取粒度。
在 iOS 开发中编译器会帮我们进行内存对齐。所以这些问题都无需考虑。但如果编译器没有提供这些功能,而且 CPU 也不支持读取非对齐数据,CPU 就会抛出硬件异常交给操作系统处理,从而产生 4610% 的差异。如果 CPU 支持读取非对齐数据,相比对齐数据,你还是要承担额外的开销造成的损失。诚然,这种损失绝不会像 4610% 那么大,但还是不能忽略的。
对象大小内存开辟:
objc对象开辟内存的主流程如下 _class_createInstanceFromZone -> instanceSize -> fastInstanceSize -> align16
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
。。。。。
}
size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
通过源码可以看到字节对齐的核心方法是align16,对齐算法流程如下
验证:
创建一个无任何属性的对象,打印内存大小为16
@interface ASTest : NSObject
@end
@implementation ASTest
@end
ASTest *test = [[ASTest alloc]init];
NSLog(@"%zd", malloc_size((__bridge const void *)(test)));
/// 输出结果如下
16
获取内存大小:
获取内存大小的方式
1、sizeof() //获取类型大小
2、class_getInstanceSize() //获取对象实际的大小,8字节对齐
3、malloc_size() //系统实际开辟内存大小 16字节对齐
sizeof()
ASTest *test = [[ASTest alloc]init];
NSString *string = @"";
NSLog(@"%zd", sizeof(int)); // int类型 占用4个字节
NSLog(@"%zd", sizeof(BOOL)); // Bool类型 占用1个字节
NSLog(@"%zd", sizeof(float)); // float类型 占用4个字节
NSLog(@"%zd", sizeof(double)); // double类型 占用8个字节
NSLog(@"%zd", sizeof(string)); // sting类型 占用8个字节
NSLog(@"%zd", sizeof(char)); // char类型 占用1个字节
NSLog(@"%zd", sizeof(short)); // short类型 占用2个字节
class_getInstanceSize()
源码调用栈如下
class_getInstanceSize -> alignedInstanceSize -> word_align
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
static inline size_t word_align(size_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
通过上面源码可以看出,内存实际占用大小是进行8字节对齐
@interface ASTest : NSObject
@end
@implementation ASTest
@end
ASTest *test = [[ASTest alloc]init];
NSLog(@"%lu", class_getInstanceSize(ASTest.class));
/// 输出结果如下
8
malloc_size()
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
通过上面源码可以看出,内存系统开辟大小是进行16字节对齐
内存对齐
内存对齐原则:
1、数据成员对齐规则:
1、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
2、结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。
3、当结构体嵌套的时候,offset是根据嵌套结构体内部的最大成员的大小来计算
2、整个结构体大小
1、最后计算结构体大小的时候,不能把嵌套结构体看成是一个整体,而要拆开来看
结构体内存对齐过程如下
struct MyStruct1 {
double a; // 8 [0 8] (0-7)
char b; // 1 [8 1] (8)
int c; // 4 [9 4] 9非4的倍数:实际(12 13 14 15)
short d; // 2 [16 2] (16 17)
}struct1;
// 内部需要的大小为: 17
// 最大属性 : 8
// 结构体大小需要是其内部最大属性的整数倍: 大于17的8的倍数 --> 24
struct MyStruct2 {
double a; //8 [0 8] (0-7)
int c; //4 [8 4] (8 9 10 11)
char b; //1 [12 1] (12)
short d; //2 [13 2] 13非2的倍数:实际(14 15)
}struct2;
// 内部需要的大小为: 15
// 最大属性 : 8
// 结构体大小需要是其内部最大属性的整数倍: 大于15的8的倍数 --> 16
struct MyStruct3 {
double a; //8 [0 8] (0-7)
int c; //4 [8 4] (8 9 10 11)
char b; //1 [12 1] (12)
short d; //2 [13 2] 13非2的倍数:实际(14 15)
int e; //4 [16 4] (16 17 18 19)
struct MyStruct1 str1; // 24 [20 24] 20非24的倍数:实际(24 47)
int f; //4 [48 4] (48 49 50 51)
}struct3;
// 内部需要的大小为: 51
// 最大属性 : 8
// 结构体大小需要是其内部最大属性的整数倍: 大于51的8的倍数 --> 56
在实际苹果系统会为MyStruct1进行如MyStruct2一样的优化,进行了属性重排
问题先行解析:
1、见👆sizeof() 部分
2、见👆malloc_size() 部分
3、见👆class_getInstanceSize() 部分
4、对象开辟的内存如果和成员变量的字节对齐一致的话,可能会导致内存溢出