内存布局
内存布局中有七个区,分别是
内核区
、堆区
、栈区
、未初始化数据(静态区)
、已初始化数据(常量区)
、代码段
、保留区
。
- 栈区:创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
- 堆区:那些由 new alloc 创建的对象所分配的内存块,它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0是系统就会回销毁该内存区域对象)。一般一个 new 就要对应一个release。在ARC下编译器会自动在合适位置为OC对象添加release操作。会在当前线程Runloop退出或休眠时销毁这些对象,MRC则需程序员手动释放。堆可以动态地扩展和收缩。
- 未初始化数据(静态区):程序运行过程内存的数据一直存在,程序结束后由系统释放
- 已初始化数据(常量区):专门用于存放常量,程序结束后由系统释放
- 代码段:用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区
- 内核区:用于加载内核代码,预留1GB
- 保留区:内存有4MB保留,地址从低到高递增
下面用代码来探索一下日常使用的数据存放在哪个区
- 栈区:
指针
,以及一些简单的基本数据类型
存储在栈区
。一般来说,地址为0x7开头
的一般都是在栈区
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"************栈区************");
// 栈区
int a = 10;
int b = 20;
NSObject *object = [NSObject new];
NSLog(@"a == \t%p",&a);
NSLog(@"b == \t%p",&b);
NSLog(@"object == \t%p",&object);
NSLog(@"%lu",sizeof(&object));
NSLog(@"%lu",sizeof(a));
}
@end
打印:
2020-02-13 16:33:11.605082+0800 001---五大区Demo[1545:519291] ************栈区************
2020-02-13 16:33:11.605169+0800 001---五大区Demo[1545:519291] a == 0x7ffee0cd87fc
2020-02-13 16:33:11.605236+0800 001---五大区Demo[1545:519291] b == 0x7ffee0cd87f8
2020-02-13 16:33:11.605298+0800 001---五大区Demo[1545:519291] object == 0x7ffee0cd87f0
2020-02-13 16:33:11.605357+0800 001---五大区Demo[1545:519291] 8
2020-02-13 16:33:11.605411+0800 001---五大区Demo[1545:519291] 4
- 堆区:
堆区
一般用来储存new出来的对象
,一般来说,堆区
的地址一般为0x6开头
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"************堆区************");
// 堆区
NSObject *object1 = [NSObject new];
NSObject *object2 = [NSObject new];
NSObject *object3 = [NSObject new];
NSObject *object4 = [NSObject new];
NSObject *object5 = [NSObject new];
NSObject *object6 = [NSObject new];
NSObject *object7 = [NSObject new];
NSObject *object8 = [NSObject new];
NSObject *object9 = [NSObject new];
NSLog(@"object1 = %@",object1);
NSLog(@"object2 = %@",object2);
NSLog(@"object3 = %@",object3);
NSLog(@"object4 = %@",object4);
NSLog(@"object5 = %@",object5);
NSLog(@"object6 = %@",object6);
NSLog(@"object7 = %@",object7);
NSLog(@"object8 = %@",object8);
NSLog(@"object9 = %@",object9);
}
@end
打印:
2020-02-13 16:33:11.605468+0800 001---五大区Demo[1545:519291] ************堆区************
2020-02-13 16:33:11.605531+0800 001---五大区Demo[1545:519291] object1 = <NSObject: 0x600003854440>
2020-02-13 16:33:11.605601+0800 001---五大区Demo[1545:519291] object2 = <NSObject: 0x600003854460>
2020-02-13 16:33:11.605655+0800 001---五大区Demo[1545:519291] object3 = <NSObject: 0x600003854450>
2020-02-13 16:33:11.605720+0800 001---五大区Demo[1545:519291] object4 = <NSObject: 0x600003854470>
2020-02-13 16:33:11.605776+0800 001---五大区Demo[1545:519291] object5 = <NSObject: 0x600003854480>
2020-02-13 16:33:11.605840+0800 001---五大区Demo[1545:519291] object6 = <NSObject: 0x600003854490>
2020-02-13 16:33:11.605902+0800 001---五大区Demo[1545:519291] object7 = <NSObject: 0x6000038544a0>
2020-02-13 16:33:11.605965+0800 001---五大区Demo[1545:519291] object8 = <NSObject: 0x6000038544b0>
2020-02-13 16:33:11.606028+0800 001---五大区Demo[1545:519291] object9 = <NSObject: 0x6000038544c0>
- 静态区:这里会存储一些定义出来但是未初始化的对象,一般来说,
0x1开头
的数据一般为常量区
和静态区
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
int clA;
static int bssA;
static NSString *bssStr1;
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"************静态区************");
NSLog(@"clA == \t%p",&clA);
NSLog(@"bssA == \t%p",&bssA);
NSLog(@"bssStr1 == \t%p",&bssStr1);
}
@end
打印:
2020-02-13 16:33:11.623869+0800 001---五大区Demo[1545:519291] ************静态区************
2020-02-13 16:33:11.623950+0800 001---五大区Demo[1545:519291] clA == 0x10ef2729c
2020-02-13 16:33:11.624016+0800 001---五大区Demo[1545:519291] bssA == 0x10ef272a0
2020-02-13 16:33:11.624067+0800 001---五大区Demo[1545:519291] bssStr1 == 0x10ef272a8
- 常量区:会存储一些定义出来且已经初始化的数据,一般来说,
0x1开头
的数据一般为常量区
和静态区
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
int clB = 10;
static int bssB = 10;
static NSString *bssStr2 = @"noah";
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"************常量区************");
NSLog(@"clB == \t%p",&clB);
NSLog(@"bssB == \t%p",&bssB);
NSLog(@"bssStr2 == \t%p",&bssStr2);
}
@end
打印:
2020-02-13 16:33:11.624130+0800 001---五大区Demo[1545:519291] ************常量区************
2020-02-13 16:33:11.624192+0800 001---五大区Demo[1545:519291] clB == 0x10ef271c0
2020-02-13 16:33:11.624244+0800 001---五大区Demo[1545:519291] bssB == 0x10ef271d0
2020-02-13 16:33:11.624300+0800 001---五大区Demo[1545:519291] bssStr2 == 0x10ef271c8
内存管理策略
TaggedPointer
为什么要使用taggedPointer?
假设要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,在64位CPU下是占8个字节的。1个字节有8位,如果我们存储一个很小的值,会出现很多位都是0的情况,这样就造成了内存浪费,苹果为了解决这个问题,引入了taggedPointer的概念。
- Tagged Pointer是苹果为了解决32位CPU到64位CPU的转变带来的内存占用和效率问题,针对NSNumber、NSDate以及部分NSString的内存优化方案。
- Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。
- Tagged Pointer指针中包含了当前对象的地址、类型、具体数值。因此Tagged Pointer指针在内存读取上有着3倍的效率,创建时比普通需要malloc跟free的类型快106倍。
- 如果想深入了解TaggedPointer请点击这里
TaggedPointer源码
// 创建
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
if (tag <= OBJC_TAG_Last60BitPayload) {
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
// 编码
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}
// 解码
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
系统对
taggedPointer
进行了_objc_encodeTaggedPointer
编码,该编码的实现就是对value进行了objc_debug_taggedpointer_obfuscator
的异或操作,而在读取taggedPointer
的时候,通过_objc_decodeTaggedPointer
进行解码,还是进行了objc_debug_taggedpointer_obfuscator
的异或操作,这样进行了两次异或操作就还原了初始值。
使用代码来验证taggedpointer类型
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
int num1 = 10;
float num2 = 12;
double num3 = 14;
long num4 = 5;
NSNumber * number1 = @(num1);
NSNumber * number2 = @(num2);
NSNumber * number3 = @(num3);
NSNumber * number4 = @(num4);
NSLog(@"number1 = %@ - %p - 0x%lx",number1,&number1,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number1)));
NSLog(@"number2 = %@ - %p - 0x%lx",number2,&number2,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number2)));
NSLog(@"number3 = %@ - %p - 0x%lx",number3,&number3,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number3)));
NSLog(@"number4 = %@ - %p - 0x%lx",number4,&number4,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number4)));
}
extern uintptr_t objc_debug_taggedpointer_obfuscator;
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
@end
打印
2020-02-13 19:35:33.310386+0800 004-taggedPointer[3472:589831] number1 = 10 - 0x7ffee4bcc7e0 - 0xb0000000000000a2
2020-02-13 19:35:33.310471+0800 004-taggedPointer[3472:589831] number2 = 12 - 0x7ffee4bcc7d8 - 0xb0000000000000c4
2020-02-13 19:35:33.310529+0800 004-taggedPointer[3472:589831] number3 = 14 - 0x7ffee4bcc7d0 - 0xb0000000000000e5
2020-02-13 19:35:33.310578+0800 004-taggedPointer[3472:589831] number4 = 5 - 0x7ffee4bcc7c8 - 0xb000000000000053
以number1为例,经过
_objc_decodeTaggedPointer
解码出来的值是0xb0000000000000a2
,可以看到倒数第二位是值,倒数第一位是类型,可以得出最后一位2、4、5、3
分别代表int long float double
类型
再来看看字符串类型
//
// ViewController.m
// 004-taggedPointer
//
// Created by cooci on 2019/4/8.
// Copyright © 2019 cooci. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSString * str1 = [NSString stringWithFormat:@"a"];
NSString * str2 = [NSString stringWithFormat:@"ab"];
NSString * str3 = [NSString stringWithFormat:@"abc"];
NSString * str4 = [NSString stringWithFormat:@"abcd"];
NSLog(@"str1 = %@ - %p - 0x%lx",str1,&str1,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str1)));
NSLog(@"str1 = %@ - %p - 0x%lx",str2,&str2,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str2)));
NSLog(@"str1 = %@ - %p - 0x%lx",str3,&str3,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str3)));
NSLog(@"str1 = %@ - %p - 0x%lx",str4,&str4,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str4)));
}
extern uintptr_t objc_debug_taggedpointer_obfuscator;
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
@end
打印:
2020-02-13 19:42:22.152170+0800 004-taggedPointer[3539:593334] str1 = a - 0x7ffee3bf27c0 - 0xa000000000000611
2020-02-13 19:42:22.152257+0800 004-taggedPointer[3539:593334] str1 = ab - 0x7ffee3bf27b8 - 0xa000000000062612
2020-02-13 19:42:22.152322+0800 004-taggedPointer[3539:593334] str1 = abc - 0x7ffee3bf27b0 - 0xa000000006362613
2020-02-13 19:42:22.152386+0800 004-taggedPointer[3539:593334] str1 = abcd - 0x7ffee3bf27a8 - 0xa000000646362614
字符串类型解压出来的值,最后一位代表的是字符串长度,而
61、62、63、64
对应的是ASCII的a、b、c、d
NONPOINTER_ISA
苹果将 isa 设计成了联合体,在 isa 中存储了与该对象相关的一些内存的信息,原因也如上面所说,并不需要 64 个二进制位全部都用来存储指针。来看一下 isa 的结构:
// x86_64 架构
struct {
uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
uintptr_t shiftcls : 44; // 存放着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
uintptr_t extra_rc : 8; // 引用计数能够用 8 个二进制位存储时,直接存储在这里
};
// arm64 架构
struct {
uintptr_t nonpointer : 1; // 0:普通指针,1:优化过,使用位域存储更多信息
uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用
uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的dealloc
uintptr_t shiftcls : 33; // 存放着 Class、Meta-Class 对象的内存地址信息
uintptr_t magic : 6; // 用于在调试时分辨对象是否未完成初始化
uintptr_t weakly_referenced : 1; // 是否被弱引用指向
uintptr_t deallocating : 1; // 对象是否正在释放
uintptr_t has_sidetable_rc : 1; // 是否需要使用 sidetable 来存储引用计数
uintptr_t extra_rc : 19; // 引用计数能够用 19 个二进制位存储时,直接存储在这里
};
注意这里的
has_sidetable_rc
和extra_rc
,has_sidetable_rc
表明该指针是否引用了 sidetable 散列表,之所以有这个选项,是因为少量的引用计数是不会直接存放在 SideTables 表中的,对象的引用计数会先存放在extra_rc
中,当其被存满时,才会存入相应的 SideTables 散列表中,SideTables 中有很多张 SideTable,每个 SideTable 也都是一个散列表,而引用计数表就包含在 SideTable 之中。
SideTables 散列表
散列表(Hash table,也叫哈希表),是根据建(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过一个关于键值得函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称作散列函数,存放记录的数组称作散列表。
来看一下 NSObject.mm
中它们对应的源码:
// SideTables
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// SideTable
struct SideTable {
spinlock_t slock; // 自旋锁
RefcountMap refcnts; // 引用计数表
weak_table_t weak_table; // 弱引用表
// other code ...
};
它们的关系如下:
看下获取一个sidetable的源码:// 获取一个sidetable
table = &SideTables()[obj];
// SideTables
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// StripedMap
template<typename T>
// 这里的模板规范了一个sidetable的样式
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
//如果是真机,StripeCount=8
enum { StripeCount = 8 };
#else
//模拟器就是StripeCount=64
enum { StripeCount = 64 };
#endif
struct PaddedT {
T value alignas(CacheLineSize);
};
PaddedT array[StripeCount];
// 散列算法
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
// 这个算法保证算出来的值是小于StripeCount的,这样就不会出现数据越界的情况
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
public:
// 重写操作符
T& operator[] (const void *p) {
// 这里返回一个sidetable
return array[indexForPointer(p)].value;
}
const T& operator[] (const void *p) const {
return const_cast<StripedMap<T>>(this)[p];
}
...
};
分离锁
分离锁并不是一种锁,而是一种对锁的用法。各个元素分别加一把锁就是我们说的分离锁。
问:为什么不用SideTables 直接包含自旋锁,引用计数表和弱引用表呢?
答:
这是因为在众多线程同时访问这个 SideTable 表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张 SideTable 的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,虽然自旋锁已经是效率非常高的锁,这会带来非常不好的用户体验。针对这种情况,将一张 SideTable 分为多张表的 SideTables,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,这就是为什么一个 SideTables 下涵盖众多 SideTable 表的原因。
自旋锁:计算机科学用于多线程同步的一种锁,线程会反复检查锁变量是否可用。由于线程在这一过程中保持执行(没有进入休眠),因此是一种忙等。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。
自旋锁适用于小型数据
、耗时很少
的操作,速度很快
。
引用计数表(RefcountMap refcnts
SideTable中包含一个
RefcountMap refcnts 中通过一个size_t(64位系统中占用64位)来保存引用计数,其中1位用来存储固定标志位,在溢出的时候使用,一位表示正在释放中,一位表示是否有弱引用,其余位表示实际的引用计数c++
Map RefcountMap refcnts
用来对象存储额外的引用计数,一个结构体weak_table_t weak_table
用来存储对象的弱引用数据
- RefcountMap refcnts 是一个C++的对象,内部包含了一个迭代器
- 其中以DisguisedPtr<objc_object> 对象指针为key,size_t 为value保存对象引用计数
- 将key、value通过std::pair打包以后,放入迭代器中,所以取出值之后,.first代表key,.second代表value
弱引用表
全局弱引用表 weak_table_t
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
弱引用表的内部结构
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
- 其中主要包含了两个属性
DisguisedPtr<objc_object> referent
对象指针,还有一个容器类保存所以只需这个对象的弱引用 - 共用体中包含两种结构体,当弱引用数量少于
4
的时候,使用数据结构来存储,当超过4
个的时候使用hash表进行存储,out_of_line_ness
默认为ob00
,当弱引用数量大于4
的时候,设置为REFERRERS_OUT_OF_LINE
ob10
,通过判断out_of_line_ness
来决定用什么方式存储 weak_referrer_t *referrers
是一个二级指针实现的hash表