5大内存分区
这里所说的内存,并非内存管理,是其他系统以及编程语言都有提及的你内存分区,是对编程语言来说比较广泛的内存说明。
- 栈区
- 堆区
- 全局区
- 常量区
- 代码区
栈区
栈区:栈区是由系统来自动分配释放,是一个栈的数据结构
- 存放的局部变量、先进后出、一旦出了作用域就会被销毁;
- 函数的参数(id self SEL_cmd)
- 函数跳转地址,现场保护等;(可被递归代用的最大方法)
- 程序员不需要管理栈区变量的内存;
- 栈区地址从高到低分配;
堆区
堆区是由开发者手动管理,或者程序结束时由系统全部回收,是一种树状的数据结构,一般用于存储malloc,new等方式创建的对象。在ios开发中,大多数内存管理问题也多出此。都是一些开发者没有及时回收内存,或者内存溢出以及泄露等问题。
- 堆区的内存分配使用的是 alloc/new开辟空间创建对象;
- 使用字符串,载入图片,或使用JSON/XML数据,使用视图都会消耗大量的堆内存。
- 需要程序员管理内存;
- ARC 的内存的管理,是编译器再便宜的时候自动添加 retain、release、autorelease;
- 堆区的地址是从低到高分配
全局区(静态储存区)
用于存放全局变量和静态变量,储存方式是:未经初始化的全局变量和静态变量存放在一个区域,初始化后的全局变量和静态变量在另一个区域。回收方式是等进程结束后由系统回收。
- 包括两个部分:未初始化 、已初始化;
- 全局区/静态区,在内存中是放在一起的,已初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域;
文字常量区
主要存储基本数据类型的值,以及常量,同样是进程结束后由系统回收。
代码区
储存要执行函数的二进制代码,如果需要执行就加载到改区域中。
通过下面代码详解
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
int a;
int b = 100;
static int c = 101;
NSString *str1 = @"hello world";
NSObject *obj = [NSObject new];
char *w1 = (char *)malloc(10);
NSLog(@"a in 全局区:%p",&a);
NSLog(@"b in 栈区:%p",&b);
NSLog(@"c in 静态区:%p",&c);
NSLog(@"str1 in 常量区:%p",&str1);
// heap: 理论由低地址到高地址拓展,实际多运行几次发现有可能出现由高地址到地地址拓展。
NSLog(@"obj in 堆:%p",obj);
NSLog(@"w1 in 堆:%p",w1);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
打印结果
a in 全局区:0x7ffeeb90fe14
b in 栈区:0x7ffeeb90fe10
c in 静态区:0x1042f79c8
str1 in 常量区:0x7ffeeb90fe08
obj in 堆:0x600000278000
w1 in 堆:0x600000278010
系统的内存分配是按照一定的逻辑来实现的,但并不是绝对的,例如图,堆区是由低地址向高地址扩展的 obj 安利应该要比 w1低一些。,实际上有可能高,这是因为堆并不是连续的内存区域,是系统用树来实前,如果 w1分配的空间大一些,则 w1的地址就会在obj之后了。
特例
1.字符串类型
多个直接声明的相同字符串在内存只占用一份内存,例如:
NSLog(@"hello1 in 常量区:%p",@"hello");
NSLog(@"hello2 in 常量区:%p",@"hello");
打印结果
hello1 in 常量区:0x10deb6048
hello2 in 常量区:0x10deb6048
这个变量的地址是在常量区存储,虽然声明的是两个字符串,看似应该开辟两端内存,但通过打印看出实际上是用同一块内存,这是可以理解的,因为这是同一个固定的字符串,在编译器就确定的了,不会更改,是一个不可变量,因此引用同一份内存并没有什么问题,如果需要在此字符串进行修改也是另外开辟一块内存。
1. block类型
block 声明的时候是在栈中的,但赋值给变量的时候会复制到堆中。
// 声明一个block
NSLog(@"block in 栈区:%p",^(){});
// 将一个block赋值给一个变量
id block = ^(){};
NSLog(@"block in 堆区:%p",block);
例子1
- (void)blockTes1 {
__block int a = 1;
int * p = &a;
^{
a = a + 1;
}();
a += 1;
NSLog(@"a = %d,*p = %d\n",a,*p);
}
结果
a = 3,*p = 3
解析
在第一个方法中,直接调用了一个block,由于block还没有被赋值,所以这时block没有被赋值到堆区。所以对于a来说也没有发生复制,与p指向同一你内容,经过两次相加都是是3;
例子2
__block int a = 1;
int * p = &a;
void (^block)(void) = ^{
a = a + 1;
};
a += 1;
block();
NSLog(@"a = %d,*p = %d\n",a,*p);
结果
a = 3,*p = 1
解析
将block赋值给一个临时变量,此时根据之前所说的内容,发生了复制,__ block 修饰的a也复制为一份新的内容,但是p依然指向之前的内容,此时p与a以已不是同一内容了,所以*p依然为1,而a经过两次相加变3.
栈与堆
当对象被创建和赋值时,数据可能从栈赋值到堆,类似的,当值仅在方法内部使用时,它们也可能从堆赋值到栈中,这个操作代价昂贵
#import "AClass.h"
@interface AClass ()
@property (nonatomic, assign) NSInteger anInteger; // 值类型
@property (nonatomic, copy) NSString *aString; // 引用传递
@end
例子1 栈复制到堆:
- (AClass *)creatAClassWithInteger:(NSInteger)i
string:(NSString *)s {
// i 在栈上
AClass *result = [AClass new];
// 赋值给属性时,它被复制到堆中,因为那是储存result地方
result.anInteger = i;
result.aString = s;
return result;
}
例子12堆复制到栈:
- (void)someMethod:(NSArray *)items {
// total 是栈内存
NSInteger total = 0;
NSMutableString *finalString = [NSMutableString string];
for (AClass *obj in items) {
//obj.anInteger 从堆复制到栈才可以使用
total += obj.anInteger;
[finalString appendString:obj.aString];
}
}
内存管理方案
tagged pointer
苹果提出了Tagged Pointer对象。由于NSNumber、NSDate一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。
tagged pointer 修饰小数据类型做的特殊处理
就引入了tagged pointer,tagged pointer是一种特殊的“指针”,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。
Tagged Pointer特点的介绍
- Tagged Pointer专门用来存储小的对象,例如NSNumber, NSDate, NSString。
- Tagged
Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free。 - 在内存读取上有着3倍的效率,创建时比以前快106倍。
为什么要引入Tagged Pointer
iPhone5s 采用64位处理器。
对于64位程序,我们的数据类型的长度是跟CPU的长度有关的。
这样就导致了 一些对象占用的内存会翻倍。
同时 维护程序中的对象需要 分配内存,维护引用计数,管理生命周期,使用对象给程序的运行增加了负担。
案例讲解
NSMutableString *mutableStr = [NSMutableString string];
NSString *immutable = nil;
#define _OBJC_TAG_MASK (1UL<<63)
char c = 'a';
do {
[mutableStr appendFormat:@"%c", c++];
immutable = [mutableStr copy];
NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
}while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);
打印结果
0xab59d8bbfe98cd26 a NSTaggedPointerString
0xab59d8bbfe9eed25 ab NSTaggedPointerString
0xab59d8bbf8aeed24 abc NSTaggedPointerString
0xab59d8bdb8aeed23 abcd NSTaggedPointerString
0xab59deedb8aeed22 abcde NSTaggedPointerString
0xab5fbeedb8aeed21 abcdef NSTaggedPointerString
0xad2fbeedb8aeed20 abcdefg NSTaggedPointerString
0xab5bf8835e89a26f abcdefgh NSTaggedPointerString
0xabd1d693fac29f2e abcdefghi NSTaggedPointerString
0x600002234cc0 abcdefghij __NSCFString
上图我们可以看到,当字符串的长度为10个以内时,字符串的类型都是NSTaggedPointerString类型,当超过10个时,字符串的类型才是__NSCFString
打印结果分析:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
上面这个方法我们看到,判断一个对象类型是否为NSTaggedPointerString类型实际上是讲对象的地址与_OBJC_TAG_MASK进行按位与操作,结果在跟_OBJC_TAG_MASK进行对比,我们在看下_OBJC_TAG_MASK的定义:
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
我们都知道一个对象地址为64位二进制,它表明如果64位数据中,最高位是1的话,则表明当前是一个tagged pointer类型。
那么我们在看下上面打印出的地址,所有NSTaggedPointerString地址都是0xd开头,d转换为二进制1110,根据上面的结论,我们看到首位为1表示为NSTaggedPointerString类型。在这里得到验证。
弱引用,引用计数
alloc:分配内存
retain:
引用计数加1
retain 底层流程
Retain源码
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
//为什么有isa?因为需要对引用计数+1,即retain+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
isa_t oldisa;
isa_t newisa;
//重点
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否为nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是 nonpointer isa,直接操作散列表sidetable
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
//dealloc源码
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
uintptr_t carry;
//执行引用计数+1操作,即对bits中的 1ULL<<45(arm64) 即extra_rc,用于该对象存储引用计数值
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
//判断extra_rc是否满了,carry是标识符
if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
//如果extra_rc满了,则直接将满状态的一半拿出来存到extra_rc
newisa.extra_rc = RC_HALF;
//给一个标识符为YES,表示需要存储到散列表
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
//将另一半存在散列表的rc_half中,即满状态下是8位,一半就是1左移7位,即除以2
//这么操作的目的在于提高性能,因为如果都存在散列表中,当需要release-1时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以直接操作extra_rc即可,不需要操作散列表。性能会提高很多
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
retain 源码分析
【第一步】判断是否为Nonpointer_isa
【第二步】操作引用计数
1、如果不是Nonpointer_isa,则直接操作SideTables散列表,此时的散列表并不是只有一张,而是有很多张(后续会分析,为什么需要多张)
2、判断是否正在释放,如果正在释放,则执行dealloc流程
3、执行extra_rc+1,即引用计数+1操作,并给一个引用计数的状态标识carry,用于表示extra_rc是否满了
4、如果carray的状态表示extra_rc的引用计数满了,此时需要操作散列表,即 将满状态的一半拿出来存到extra_rc,另一半存在 散列表的rc_half。这么做的原因是因为如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,这么对半分操作的目的在于提高性能
散列表
- sidetable 包括自旋锁(spinlock_t)
- 引用计数表(refcountMap)
- 弱引用表(weak_table_t)
散列表为什么在内存有多张?最多能够多少张?
- 如果散列表只有一张表,意味着全局所有的对象都会存储在一张表中,都会进行开锁解锁(锁是锁整个表的读写)。当开锁时,由于所有数据都在一张表,则意味着数据不安全
- 如果每个对象都开一个表,会耗费性能,所以也不能有无数个表
为什么在用散列表,而不用数组、链表?
- 数组:特点在于查询方便(即通过下标访问),增删比较麻烦(类似于之前讲过的methodList,通过memcopy、memmove增删,非常麻烦),所以数据的特性是读取快,存储不方便
- 链表:特点在于增删方便,查询慢(需要从头节点开始遍历查询),所以链表的特性是存储快,读取慢
- 散列表的本质就是一张哈希表,哈希表集合了数组和链表的长处,增删改查都比较方便,例如拉链哈希表(在之前锁的文章中,讲过的tls的存储结构就是拉链形式的),是最常用的,如下所示
Dealloc
Dealloc源码
inline void
objc_object::rootDealloc()
{
//对象要释放,需要做哪些事情?
//1、isa - cxx - 关联对象 - 弱引用表 - 引用计数表
//2、free
if (isTaggedPointer()) return; // fixme necessary?
//如果没有这些,则直接free
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
//如果有
object_dispose((id)this);
}
}
源码分析
//对象要释放,需要做哪些事情?
//1、isa - cxx - 关联对象 - 弱引用表 - 引用计数表
//2、free
dealloc 源码分析
在retain和release的底层实现中,都提及了dealloc析构函数,下面来分析dealloc的底层的实现
进入dealloc -> _objc_rootDealloc -> rootDealloc源码实现,主要有两件事:
根据条件判断是否有isa、cxx、关联对象、弱引用表、引用计数表,如果没有,则直接free释放内存
如果有,则进入object_dispose方法
object_dispose 源码
id
object_dispose(id obj)
{
if (!obj) return nil;
//销毁实例而不会释放内存
objc_destructInstance(obj);
//释放内存
free(obj);
return nil;
}
👇
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
//调用C ++析构函数
if (cxx) object_cxxDestruct(obj);
//删除关联引用
if (assoc) _object_remove_assocations(obj);
//释放
obj->clearDeallocating();
}
return obj;
}
👇
inline void
objc_object::clearDeallocating()
{
//判断是否为nonpointer isa
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是,则直接释放散列表
sidetable_clearDeallocating();
}
//如果是,清空弱引用表 + 散列表
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
👇
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//清空弱引用表
weak_clear_no_lock(&table.weak_table, (id)this);
}
if (isa.has_sidetable_rc) {
//清空引用计数
table.refcnts.erase(this);
}
table.unlock();
}
object_dispose源码分析
进入object_dispose源码,其目的有以下几个
销毁实例,主要有以下操作
调用c++析构函数
删除关联引用
释放散列表
清空弱引用表
free释放内存
release
release 源码
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
bool sideTableLocked = false;
isa_t oldisa;
isa_t newisa;
retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//判断是否是Nonpointer isa
if (slowpath(!newisa.nonpointer)) {
//如果不是,则直接操作散列表-1
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
//进行引用计数-1操作,即extra_rc-1
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
//如果此时extra_rc的值为0了,则走到underflow
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));
if (slowpath(sideTableLocked)) sidetable_unlock();
return false;
underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate
// abandon newisa to undo the decrement
newisa = oldisa;
//判断散列表中是否存储了一半的引用计数
if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}
// Transfer retain count from side table to inline storage.
if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}
// Try to remove some retain counts from the side table.
//从散列表中取出存储的一半引用计数
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.
if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
//进行-1操作,然后存储到extra_rc中
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}
if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}
// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}
//此时extra_rc中值为0,散列表中也是空的,则直接进行析构,即自动触发dealloc流程
// Really deallocate.
//触发dealloc的时机
if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
if (slowpath(sideTableLocked)) sidetable_unlock();
__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);
if (performDealloc) {
//发送一个dealloc消息
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}
release 源码分析
分析了retain的底层实现,下面来分析release的底层实现
- 通过setProperty -> reallySetProperty -> objc_release -> release ->
rootRelease -> rootRelease顺序,进入rootRelease源码,其操作与retain 相反 - 判断是否是Nonpointer isa,如果不是,则直接对散列表进行-1操作
- 如果是Nonpointer isa,则对extra_rc中的引用计数值进行-1操作,并存储此时的extra_rc状态到carry中
- 如果此时的状态carray为0,则走到underflow流程
- underflow流程有以下几步:
- 判断散列表中是否存储了一半的引用计数
- 如果是,则从散列表中取出存储的一半引用计数,进行-1操作,然后存储到extra_rc中
- 如果此时extra_rc没有值,散列表中也是空的,则直接进行析构,即dealloc操作,属于自动触发
retainCount
retainCount源码
进入retainCount -> _objc_rootRetainCount -> rootRetainCount源码,其实现如下
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
👇
uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);
return obj->rootRetainCount();
}
👇
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
//如果是nonpointer isa,才有引用计数的下层处理
if (bits.nonpointer) {
//alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
//如果不是,则正常返回
sidetable_unlock();
return sidetable_retainCount();
}