垃圾回收
对于c
语言内存需要手动去管理申请(malloc/calloc
)/释放(free
),容易导致忘记释放或者重复释放,进而引发”内存泄漏“或者”进程异常崩溃“等,且容易出现”内存碎片“。
像对于已经使用完成但未释放的内存对象(即不再存活的对象)如同”垃圾“一样存在内存中且占用内存,若”垃圾“内存不及时释放占用多,就会导致进程内存紧张,进而引发”OOM
“;
常见的”垃圾回收“判定方式如下:
-
引用计数
对每一个创建的对象分配一个
引用计数器
,用来存储对象被引用的次数,若计数为0,则标记为”垃圾“对象;但这种存在”循环引用“问题,且对象已经不再被使用,导致内存泄露,需要手动管理该对象,如弱引用或者手动释放;objective-c
及c++11
智能指针采用了此方案; -
可达性分析
这种方案是目前主流语言里采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。
可达性
java
采用了此垃圾回收机制,在后台创建一个守护进程。该进程会在内存紧张的时候自动跳出来,把堆空间的垃圾全部进行回收,从而保证程序的正常运行。并通过内存区域分区(包含新生代和老生代)来解决内存碎片问题: 新生代:存活对象少、垃圾多,采用
复制回收
机制;老年代:存活对象多、垃圾少,采用
标记整理
机制;
自动引用计数
在MRC
时代,对于内存的管理需要通过retain/release/autorelease
方式去手动调用管理,难免存在内存泄露及野指针导致的进程异常,因此,苹果引入了自动引用计数
,c++11
引入智能指针内存回收机制;
c++11智能指针实现
每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,引用计数加1;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
原文链接:C++11--智能指针详解及实现
自动引用计数ARC(Automatic Reference Counting)
xcode
已默认开启ARC
模式,苹果的LLVM
编译器自动添加retain/release
代码,无需手动添加(会编译报错),降低了开发工作量同时减少了内存泄露及进程异常崩溃的风险;
xcode
配置工程ARC
模式的选项为:Objective-C Automatic Reference Counting
,若当文件不开启ARC
,则需要添加fobjc-no-arc
选项标志;
引用计数原理实现
alloc/retain/release/dealloc
源码来源于:objc4-779.1
alloc
调用栈如下:
alloc
_objc_rootAlloc
callAlloc
objc_msgSend->allocWithZone
_objc_rootAllocWithZone
_class_createInstanceFromZone
calloc
initInstanceIsa
核心函数如下:
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);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);//分配内存
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);//初始化isa指针指向类对象
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
主要是获取类实例对象的大小并分配内存同时修改isa
指针;
retain
实现调用栈如下:
retain
obj->rootRetain
objc_object::rootRetain
核心代码如下:
inline id
objc_object::rootRetain()
{
//判断是否为taggerPointer,若是则返回实例对象自身
if (isTaggedPointer()) return (id)this;
//散列表引用计数
return sidetable_retain();
}
具体关于taggerPointer
指针可参考深入理解Tagged Pointer,特点如下:
对于sidetable_retain
函数如下:
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
//全局对象获取对应的table,其中内部使用
SideTable& table = SideTables()[this];
//内部锁加锁,其中使用了spinlock_t自旋锁,以保证原子性获取引用计数属性refcnts
table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
实质就是通过静态全局SideTablesMap
对象中的SideTable
对象中的refcnts
属性来记录引用计数,retain
就是原子性的+1操作,具体的SideTable
对象内部结构可参考分析iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-
release
核心调用如下:
objc_object::rootRelease()
{
if (isTaggedPointer()) return false;
return sidetable_release(true);
}
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
ASSERT(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
auto &refcnt = it.first->second;
if (it.second) {
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}
release
实质就是引用计数减1且调用dealloc
;
autorelease
核心调用如下:
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
//由AutoreleasePoolPage类来管理释放
return AutoreleasePoolPage::autorelease((id)this);
}
实质就是就是将这个对象加入到当前AutoreleasePoolPage类实例对象的栈顶next指针指向的位置,等到下一次runloop
触发调用,具体原理实现可参见黑幕背后的Autorelease;
dealloc
核心代码如下:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return;
object_dispose((id)this);
}
id
object_dispose(id obj)
{
if (!obj) return nil;
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
objc_destructInstance(obj);
free(obj);
return nil;
}
实质就是对象析构并释放内存,不要显式调用该函数且ARC
模式下dealloc
中隐含调用[super dealloc]
;
ARC
ARC属性
属性修饰符对应的所有权修饰符关系如下:
其中
id
及对象类型默认为__strong
类型;
-
assign
,针对数值类型,如CFFloat NSIngter等; -
strong
,表示”拥有关系“,为这种属性设置新值时,会先保留新值,然后释放旧值,最后将新值设置上去; -
retain
,作用是一样的,只是写法上的区别。在非ARC
机制时,是用retain
关键字修饰;在ARC
机制后,一般都用strong
关键字来代替retain
了; -
weak
,表示”非拥有关系“,为这种属性设置新值时,不会保留新值,也不会释放旧值;不过属性对象释放时,会被置为nil
,一般用于解决循环引用问题; -
unsafe_unretained
,语义同assign
相同,只是简单地赋值操作,但其用于对象类型,且对象被释放时,与weak
不同的是,不会自动清空(因此称为unsafe
); -
copy
,语义与strong
类似,但设置方法时不会保留新值,而是将其拷贝;
手动释放对象
ARC
解决大部分内存管理问题,但对于Core Foundation
对象(使用c
编写的对象)无法自动管理,需要通过CFRetain/CFRelease
来增加/减小引用计数;另外,Core Foundation
对象与objective-c
对象需要桥接转换处理,称为"Toll-Free Bridge
",转换处理需要告诉编译器如何处理引用计数,关键词如下;
-
__bridge
: 只做类型转换,不修改相关对象的引用计数,原来的Core Foundation
对象在不用时,需要调用CFRelease
方法; -
__bridge_retained
:类型转换后,将相关对象的引用计数加 1,原来的Core Foundation
对象在不用时,需要调用CFRelease
方法; -
__bridge_transfer
:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation
对象在不用时,不再需要调用 CFRelease 方法;
dealloc
内部需要释放对象内部持有的ARC
不能自动管理的对象,如:
- 移除通知中心的监听
- 移除
KVO
监听 - 取消定时器,并将定时器置空(nil),
NSTimer
,GCDTimer
- 释放非Objective-C对象的内存,如
CFRelease(...), free(...)
- 释放GCD队列:
dispatch_release(_ioQueue)
;
内存泄露分析
内存泄露分析常用的工具,如Instrument->Leak
及僵尸对象;
Leak
leak
提供了直观的分析内存泄露的循环引用关系、调用树等分析手段;
僵尸对象
对已回收的内存对象再次访问有时会出现非法访问的情况导致进程异常崩溃,若同时导致栈紊乱,就难以通过崩溃报告查看具体的崩溃位置及相应的非法访问代码处,所幸苹果提供了”僵尸对象“工具来分析对已回收对象的非法访问。
具体的原理如下:
运行期系统会把所有已经回收的实例转化为”僵尸对象“且不回收它们,若僵尸对象收到消息后,会抛出异常,其中包含了准确的发送过来的消息,并描述了回收之前的对象;
实现原理是通过方法调配dealloc
方法执行一段自定义代码,其中代码执行:
- 获取发送消息类对象名称
clsName
- 拷贝
_NSZombie_
类并赋予其新的名称_NSZombie_clsName
,该类如同NSobject
类同属根类,同时存在唯一的isa
实例变量,且没有实例方法,因此向该类发送消息都会走”完整的消息转发流程“; - 消息转发流程中
__forwarding__
方法中截获类名前缀为__NSZombie_
的类,则表明消息接收者为僵尸对象;
xcode
中的Zombies
工具检测出僵尸对象就会弹出此提示: