OC内存管理(一) 内存的分区布局和管理方案

215 阅读6分钟

内存分区与布局

image.png

栈区(Stack)

  • 栈区是一块连续的内存空间,它的结构是从高地址往低地址拉伸,遵循先进后出(FILO)原则
  • 栈区存储的是局部变量函数方法参数指针
  • 栈区的地址空间一般是以0x7开头
  • 栈区由编译器自动分配内存和释放

堆区(Heap)

  • 堆区是一块不连续的内存空间,它的结构是从低地址向高地址扩展
  • 堆区存储的是对象,需要开辟空间的东西,栈区内存比较小,堆区比较大,所以在堆区开辟空间

全局区(静态区)

  • 堆区地址空间一般以0x6开头
  • 全局区又分为.bss段.data段,内存地址一般由0x1开头:
    • Bss段: 未初始化的全局变量,静态变量,
    • Data段: 已初始化的全局变量,静态变量
  • 静态区也被称之为静态安全区,因为在多个文件使用同一个静态变量,系统会各自生成一个相同初始值地址不同的静态变量,这样在各自文件内使用就不会互相干扰,数据比较安全。

常量区(.rodata)

  • 常量区的内存在编译时就已经确定,主要存放已经使用过的,且没有指向的字符串常量(因为字符串常量可能在程序中多次被使用,所以在程序运行之前就会提前分配内存)。常量区的常量在程序结束后,由系统释放

代码区(.text)

  • 存储程序代码,在编译时加载到内存中,代码会被编译成二进制的形式进行存储

注意

    1. 我们可以从图中看出,栈底的内存为0c0000000,转成10进制后等于3GB,还有1G的内存分配给了内核区
    • 内核区:主要进行消息处理以及多线程的操作
    1. 代码区的地址是从0x00400000开始,怎么不是从0x0开始? 因为0x0nil,从nil开始分配会出现问题,所以在代码区之前就有了保留区
    • 保留区:预留给系统处理nil

内存管理方案

TaggedPointer(WWDC2020)

  • TaggedPointer是字面意思就是标记的指针,我们知道正常的指针指向的是内存,但对于有些类型例如NSNumber,它不需要一般对象的cache_tmethod_list等,它只是一个值完全可以存在指针中,这样也大大的优化访问速度与内存,于是就产生了TaggedPointer
    • TaggedPointer主要针对一些小对象类型,例如NSNUmberNSDateNSIndexPath等,
    • 小对象将值存在本身的指针中,没有另外调用Malloc开辟或者free释放内存,
    • TaggedPointer3倍内存空间的优化
    • 相比于allocate/destoryTaggedPointer的速度要快106倍
    • Tagged Pointer指针的值不再是地址了,⽽是真正的值。实际上它不再是⼀个对象了,只是⼀个披着对象⽪的普通变量⽽已。所以它的内存并不存储在堆中,也不需要malloc和free
    • taggedPointer不会引起引用计数的变化,不会产生野指针导致崩溃

NONPOINTER_ISA

  • 说到taggedPointer往往会联想到NONPOINTER_ISA,isa的种类和结构。
    • NONPOINTER_ISA表示是否对isa指针开启指针优化: - 0:纯isa指针 - 1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等
    • NONPOINTER_ISA也是对存储进行了优化,使得isa中的64位得到充分的使用,其中的shiftclstaggedPointer中的Payload类似,用来存储有效数据
  • NONPOINTER_ISA中的extra_rc存储的是引用计数相关数据。

Retain、Release、retainCount、散列表

objc_object::rootRetain

// tryRetain: false, variant: Fast
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;
    
    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) { ... }  //FastOrMsgSend类型的操作
    if (slowpath(!oldisa.nonpointer)) { ... } // 非nonpointer操作

    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) { ... } //正在析构
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                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;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (variant == RRVariant::Full) { // extra_rc 存满了
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }

        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}
  • do-while循环代码先将oldisa赋值给newisa,如果newisa非nonpointer类型,会根据判断走sidetable_tryRetain或者sidetable_retain方法,如果extra_rc存满会走carry判断进行extra_rc存一半,循环外散列表存一半操作。先来看看sidetable_tryRetain方法
  • sidetable_tryRetain:
bool
objc_object::sidetable_tryRetain()
{
//-   该方法的主要作用拿到当前对象的散列表中的引用计数表`SideTable`,然后判断是否在析构,判断没有存满的话就引用计数增加
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this]; // 拿到当前对象的散列表

    bool result = true;
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE); // 拿到SideTable中的引用计数表`RefcountMap`
    auto &refcnt = it.first->second; // 引用计数表中的size_t
    if (it.second) {
        // there was no entry
    } else if (refcnt & SIDE_TABLE_DEALLOCATING) { // 正在析构
        result = false;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) { // 没有存满
        refcnt += SIDE_TABLE_RC_ONE; // 引用计数增加
        /**
        #define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0) //是否有过 weak 对象 
        #define SIDE_TABLE_DEALLOCATING (1UL<<1) //表示该对象是否正在析构 
        #define SIDE_TABLE_RC_ONE (1UL<<2) //从第三个 bit 开始才是存储引用计数数值的地方 ,第三个 bit 开始才是存储引用计数数值的地方
        #define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1)) // 最高位,表示在最高位占满,用来判断是否存满
        */
    }
  
    return result;
}
  • sidetable_retain
id
objc_object::sidetable_retain(bool locked)
{
//该流程也是获取对象的散列表,然后进行引用计数增加。
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this]; // 获取对象的散列表
  
    if (!locked) table.lock(); // 如果没有加锁,则调用散列表的互斥锁进行加锁
    size_t& refcntStorage = table.refcnts[this]; // 获取当前引用计数
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) { //判断是否存满
        refcntStorage += SIDE_TABLE_RC_ONE; // 引用计数增加
    }
    table.unlock(); // 解锁
    return (id)this;
}

后面再执行newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry)extra_rc进行赋值,RC_ONE是开始存储extra_rc的位置。addc函数的作用是将newisa.bitsRC_ONE相加并存入newisa.bits当存满了carry值就为1。如果存满会将引用计数在extra_rc中存储一半然后做相关的标记,然后调用sidetable_addExtraRC_nolock方法

  • sidetable_addExtraRC_nolock: 主要是从引用计数表获取引用计数后,将另一半引用计数存入相应的位置
bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this]; // 获取对象的散列表

    size_t& refcntStorage = table.refcnts[this]; // 从引用计数表获取引用计数
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true; // 如果存满直接返回

    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry); // delta_rc << SIDE_TABLE_RC_SHIFT 是从第二个位置开始存
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK); // 溢出的话就refcntStorage为可存储的最大值
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

retain流程图

image.png

objc_object::rootRelease

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false; // 先判断是不是小对象
    bool sideTableLocked = false;
    isa_t newisa, oldisa;
    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) { ... }
    if (slowpath(!oldisa.nonpointer)) { ... } 、、 是不是`nonpointer`

retry:
    do {
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        if (slowpath(newisa.isDeallocating())) { ... }

        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (slowpath(newisa.isDeallocating()))
        goto deallocate;
    ...
    return false;
 underflow:
    newisa = oldisa;
    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }
        if (!sideTableLocked) { // 未加锁相关赋值后再进行 retry
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            oldisa = LoadExclusive(&isa.bits);
            goto retry;
        }
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF); // 从散列表借一半
        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there

        if (borrow.borrowed > 0) {
            bool didTransitionToDeallocating = false;
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;

            bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
            ...
            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                ClearExclusive(&isa.bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa.bits);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();

            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

deallocate:
    ...
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}
  • 如果不是nonpointer就调用sidetable_release方法进行引用计数减少操作,代码如下
objc_object::sidetable_release(bool locked, bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    if (!locked) 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) { // 如果是week对象
        // 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; // 引用计数减1
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
    // `do_dealloc`为`true`且`performDealloc`为`true`则发送`dealloc`消息.
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc)); // 调用dealloc
    }
    return do_dealloc;
}
  • 再调用subc进行extra_rc--,如果减多了调用sidetable_subExtraRC_nolock向散列表借一半再对extra_rc进行赋值,代码如下:
uintptr_t
objc_object::sidetable_subExtraRC_nolock(size_t delta_rc)
{
    ASSERT(isa.nonpointer);
    SideTable& table = SideTables()[this]; // 获取散列表

    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()  ||  it->second == 0) { // 散列表没有
        // Side table retain count is zero. Can't borrow.
        return { 0, 0 };
    }
    size_t oldRefcnt = it->second;

    // isa-side bits should not be set here
    ASSERT((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    ASSERT((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    size_t newRefcnt = oldRefcnt - (delta_rc << SIDE_TABLE_RC_SHIFT); // 借一半,剩余一半再存入
    ASSERT(oldRefcnt > newRefcnt);  // shouldn't underflow
    it->second = newRefcnt;
    return { delta_rc, newRefcnt >> SIDE_TABLE_RC_SHIFT };
}

release流程图

image.png

dealloc源码分析

delloc->_objc_rootDealloc->rootDealloc

image.png

  • 1.根据条件判断是否有isa、cxx、关联对象、弱引用表、引用计数表,如果没有,则直接free释放内存
  • 2.如果,则进入object_dispose方法

object_dispose

image.png

object_destructInstance

image.png

clearDeallocating

image.png

clearDeallocating_slow

image.png 通过上面可以看到,object_dispose方法的目的有一下几个

  • 1.销毁实例,主要有以下操作
    • 1.调用c++析构函数
    • 2.删除关联引用
    • 3.释放散列表
    • 4.清空弱引用表
  • 2.free释放内存

retainCount

  • retainCount主要调用流程如下:retainCount -> _objc_rootRetainCount -> rootRetainCount,核心代码如下:
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
    if (bits.nonpointer) { 
        uintptr_t rc = bits.extra_rc;
        if (bits.has_sidetable_rc) { // 如果有散列表,retainCount值为 extra_rc 和引用计数表里的值相加
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount(); // 如果是非`nonpointer`,则直接取散列表
}
inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
    if (bits.nonpointer) { 
        uintptr_t rc = bits.extra_rc;
        if (bits.has_sidetable_rc) { // 如果有散列表,retainCount值为 extra_rc 和引用计数表里的值相加
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount(); // 如果是非`nonpointer`,则直接取散列表
}
  • 如果是isTaggedPointer直接返回,如果是nonpointer类型,则取extra_rc,如果在散列表中存有,则取散列表中的值和extra_rc的和
  • 无论是sidetable_getExtraRC_nolock还是sidetable_retainCount方法里面都有引用计数值 >> SIDE_TABLE_RC_SHIFT的操作,主要是因为从存的时候从第二位开始存,所以计算retainCount时需要右移SIDE_TABLE_RC_SHIFT

散列表

散列表SideTable是一个结构体,主要由互斥锁RefcountMap引用计数表,和weak_table_t弱引用表三个成员变量组成,还有相关的析构函数和加锁方法:

image.png