本文将介绍,内存分布、内存管理
一、内存分布
内存主要分为五大区,按照地址从高向低依次为:栈区 -> 堆区 -> 全局区 -> 常量区 -> 代码区(__text)
补
这里内存指的是程序加载到cpu时的虚拟内存
iOS应用的虚拟内存默认分配4G大小,五大区占3G,还有1G是五大区之外的系统内核区
每个区放置的内容不一样
- 栈区:函数,方法,局部变量,对象指针。由系统自动管理(高地址像地址扩展,是一块连续的内存区域)
- 堆区:通过alloc、malloc、realloc开辟的对象,是不连续的内存区域,以链表结构存在。手动管理(目前ARC自动管理);
- 全局区:全局变量,静态变量,空间由系统管理,static修饰的变量仅执行一次,生命周期为整个程序运行期。
- 常量区:常量(整型、字符型,浮点,字符串等))。空间由系统管理,生命周期为整个程序运行期。
- 代码区(.text):存放代码的区域,编译完后,是cpu可执行的指令。
补:
1、全局区也叫静态区分为DATA段和BSS段。DATA段(全局初始化区)存放初始化的全局变量和静态变量;BSS段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中,BSS段在程序执行之前会被系统自动清零,所以未初始化全局变量和静态变量在程序执行之前已经为0。
2、在其他文件中定义的全局变量,在本文件中更改,只对本文件有效。
通过一些例子可以测试不同区的地址不同
1、栈区和堆区
image.png
2、全局静态区和常量区
image.png
可以看出,
栈区
是以0x7
开头的地址,
全局区
和常量区
一般以0x1
开头的地址,
堆区
以0x6
开头的地址
二、内存管理方案
先介绍两个概念:TaggedPointer和Nonpointer
nonpointer
:其实指的是使用nonpointer-isa(非指针对象,对isa进行地址优化对象),我们一般创建的对象都是这个nonpointer对象
Taggedpointer
:小对象,短的string,NSNumber和NSDate对象
它的指针的值不再是地址,而是真正的值。所以实际上它不再是一个对象,只是叫做对象,其实只是一个普通变量。它的内存并不存储在堆中,所以不需要malloc和free。
下面我们用NSString
的一个例子来说明这两种类型
同样的对self.nameStr赋值,执行第二个,页面就崩溃了 image.png
且崩到了objc_release里,是因为这里存在过度释放。我们可以看下这里的namestr类型
第一个
第二个
image.png
同一个对象,类型变了?是的。
- 因为第一个里面,字符串比较短,所以系统会安排其为小对象(TaggedPointer),第二个字符串比较长,所以安排其为nonpointer对象。
- 所以第一个方法里,namestr不是一个真正的对象,只是一个常量,不需要set、get方法,由系统负责管理内存空间。
- 而第二个方法中,namestr是一个对象,赋值时,调用set方法(新值的retain,旧值的release),所以多线程操作时,可能存在上一个旧值刚release完,其他线程又要release,导致过度释放,所以崩溃了。
-
那么多少长度的string就切换指针类型呢,如下
image.png
2.1、分析taggedpointer
之所以用小对象,是因为,正常对象要占8个字节,就是64位,而有些值根本用不完64位,所以就用小对象(地址里就包含值),可以节省内存,提高性能。
先来打印几个小对象看下内存地址
image.png
以上的a、b都是小对象~
但从打印结果看来,值和地址间也看不出有关联关系。😄当然表面看不出来啦,因为taggedpointer在初始化时肯定要进行混淆~
1、结构
到objc源码中看下taggedpointer的初始化,可以大致看出,进行了混淆。
- 再去搜这个
objc_debug_taggedpointer_obfuscator
image.png - 看到了tagpointer指针地址的解码和编码方法,用的是异或,那么两次异或就会还原指针地址。我们就在外面用一下解码函数
_objc_decodeTaggedPointer_
,拿出string真正的指针地址
- 结果显示:61就是a的ASCII码,62就是b的ASCCII码,那么taggedpinter指针包含了值!
再试一下number的地址
-
nstring和nsnumber的头部不一样(0xa和0xb),这又代表什么?
猜测是为了表示是否是tagged指针,去源码中搜索
image.png -
找到判断函数,其中
# define _OBJC_TAG_MASK (1UL<<63)
代表这个mask是最高位是1,那么上面那个isTaggedPointer函数里的算法意思就是,只要最高位为1,那么它就是tagged指针类型 -
0xa和0xb化为二进制分别为(1010,1011),最高位都是1,所以它们都是tagged类型。(此处也可以验证非tagged类型的值)
-
最高位用来确定了tagged类型,那么后面10、11又用来代表什么呢?猜测是为了代表不同的类型(NSString和NSSNumber)
找到判断类型的函数
image.png
点进 OBJC_TAG_Last60BitPayload
这个判断条件,
- 果然是用来确定类型的,下面验证一下NSDate,是否是这套逻辑,
image.png
其中(e:1110
)-> 最高位是1
,说明是tagged
指针,后面三位是6
,对照上面的enum,是TAG_NSDate
!
由此,我们可以得到taggedPointer的结构
1、指针地址
2、tagged类型的flag
3、值
4、是否是tagged
- 这样的一个类型,包含了这么多信息,而且是存在
常量区
,由系统自动管理
,读取的效率
是相当高
的。根据官方,是非taggedpointter的3倍,创建的速度比非tagged快
106倍。
所以日常开发中,给NSString、NSNumber、NSDate
赋值时,尽可能直接使用常量,有助于提高性能
。
2.2、分析nonponiter内存管理
说到内存管理,自然想到引用计数,和引用计数相关的,就是这几个操作:alloc,retain、release、dealloc
使用最多的set方法就是包含了新值的retain,旧值的release,那么就从set方法开始
从源码中可以得到set流程(此源码可以自己查看):
objc_setProperty
-> reallySetProperty
-> objc_retain(newValue)
->objc_release(oldValue)
2.2.1 objc_retain
源码显示,objc_retain
又调用了retain
-> rootRetain
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//判断是否是tag,如果是,直接返回
if (isTaggedPointer()) return (id)this;
bool sideTableLocked = false;
bool transcribeToSideTable = false;
//因为引用计数存在isa里的extra_c里
isa_t oldisa;
isa_t newisa;
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
//如果不是nonpointer,直接操作散列表对引用计数操作+1
if (slowpath(!newisa.nonpointer)) {
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
//如果正在释放,清空散列表,
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;
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.
//将另一半存在散列表的中,即满状态下是8位,RC_HALF=一半就是1左移7位,即除以2
//这么操作的目的在于提高性能,因为如果都存在散列表中,当需要release时,引用计数-1,都需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc存储一半的话,可以优先直接操作extra_rc即可,不需要操作散列表。性能会提高很多
sidetable_addExtraRC_nolock(RC_HALF);
sidetable_addExtraRC_nolock(RC_HALF);
}
if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}
以上主要分为几个步骤
- 判断是否时taggedPointer,如果是,则直接返回自身,不操作任何
- 判断是否是Nonpointer_isa(do-while)
- 引用计数操作
1、如果不是nonponinter_isa,直接操作散列表SideTable。进行开锁解锁。
2、判断是否正在释放,如果是,调用dealloc,
3、如果不是,则对extra_c➕1操作,并给一个引用计数的状态标识carry,用于表示extra_rc是否满了
4、如果extra_rc满了,那么操作散列表,将一半的引用计数存在散列表里。
下面查看释放过程
2.2.2 objc_release
搜索objc_release
,可以得到调用流程:objc_release
->release
-> rootRelease
rootRelease源码:
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;
}
以上分为这几个步骤
- 判断是否是
taggedPointer
,若是,返回no,不做任何操作, - 判断时否是
nonPointer
,如果不是,直接操作散列表side table
,引用计数-1
, - 如果是nonPointer,则
1、对extra_rc -1
,并存储当前extra_rc
的状态为carry
(一直减1,直到extrc_rc==0
时跳到下一步)
2、extrc_rc
==0,跳到underflow
underflow
:
3、判断散列表中是否存储了一半
的引用计数,如果是的,则从散列表中取出存储的一半
引用计数,-1
操作,存储到extra_rc
中
4、如果散列表中为空
,而此时extra_rc也为空
,则直接进行析构
,即自动触发dealloc
操作
从retain和release操作,可以发现这是两个相反
的操作流程,那么其中的散列表sideTable
具体是啥呢?
继续往下分析
2.3、散列表sideTable
从2.2中,知晓sideTable的作用是用于
一个是非nonpointer_isa对象的引用计数使用
另外一个重要的作用是 nonpointer时,当引用计数值过大
时,会将一半
的引用计数存
到它里面
我们先去看下stable的结构
struct SideTable {
spinlock_t slock;//开/解锁
RefcountMap refcnts;//引用计数表
weak_table_t weak_table;//弱引用表
....
}
从类型看出,它是一个结构体,包含了锁
、引用计数表
、弱引用表
,所以上面的引用计数都存到它其中的引用计数表中了。
那么它就是一张表么,还是多个?
- 查看
sidetable_unlock
方法,定位到SideTables
,
objc_object::sidetable_unlock()
{
SideTable& table = SideTables()[this];
table.unlock();
}
- 看出
SideTables
其实是一个数组,在操作开|解锁时,其实只是操作其中一张表
再看一下SideTables的获取
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;
static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}
- 是由
StripedMap
通过get方法获取
再看一下StripedMap
结构
- 内存中最多只有8张散列表(真机),64张(非真机),并且重构了
[ ]
操作符,直接通过对象内存地址通过indexForPointer
得到下标,再使用[ ]
获取到对应的sidetable
2.3.1 为什么只有8张表(真机)
- 如果每个对象都对应一张散列表,首先那占用内存很多,第二,每次操作引用计数时都要开/解锁,对整个程序性能不好
- 如果整个内存只有一张散列表共用,那么每个对象操作时,都要开/解锁,会暴露所有对象的引用计数、弱引用等信息,不安全~
2.3.2、散列表是属于哪种表结构
- 散列表是一种
哈希表
,key是关联对象内存地址
的。哈希表的特点就是:查询快
、增删改方便·,整体性能好
。(比如于tls,存储结构就是拉链形式的) - 而没有使用链表和数组,因为
链表
特点是:找到节点增删改方便
,但查询慢
(需要从头节点开始遍历查询),它属于存储快
,读取慢
。而数组
特点是:查询方便
(即通过下标访问),增删改
比较麻烦
,它属于读取快
,存储改不方便
。
2.3.3、上面retain过程为什么只存储一半引用计数到表里
- 为了提高性能
extra_rc
的引用计数满
了,就需要操作散列表,将满状态的一
半拿出来存
到extra_rc
,另一半
是存
到散列表
中。是因为如果都
存储在散列表
,每次对散列表操作都需要开/解锁
,操作耗时
,消耗性能
大,这么一半分
操作目的
就是提高性能
*以上是散列表的补充,那么还有一个重要的函数dealloc
2.3、dealloc分析
搜索源码中dealloc
得到调用顺序:dealloc -> _objc_rootDealloc -> object_dispose
rootDealloc源码:
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
//没有弱引用表、关联对象、c++函数、引用计数表,直接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 {
//如果有任何一个,调用dispose
object_dispose((id)this);
}
}
object_dispose源码:
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
再跳入objc_destructInstance源码:
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()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
//如果不是nonpoint,则直接释放散列表
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// 如果是nonponter,清空弱引用表 + 散列表
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}
assert(!sidetable_present());
}
dealloc步骤
- 调用c++析构函数
- 删除关联对象引用
- 释放引用计数表
- 清空弱引用表
- free释放自己
至此,整个retain-release流程分析完毕,下面归纳一下流程
后补
网友评论