内存管理

本文将介绍,内存分布、内存管理

一、内存分布

内存主要分为五大区,按照地址从高向低依次为:栈区 -> 堆区 -> 全局区 -> 常量区 -> 代码区(__text)

image.png


这里内存指的是程序加载到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的一个例子来说明这两种类型

image.png

同样的对self.nameStr赋值,执行第二个,页面就崩溃了

image.png

且崩到了objc_release里,是因为这里存在过度释放。我们可以看下这里的namestr类型

第一个

image.png

第二个
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的初始化,可以大致看出,进行了混淆。

image.png

  • 再去搜这个objc_debug_taggedpointer_obfuscator
    image.png
  • 看到了tagpointer指针地址的解码和编码方法,用的是异或,那么两次异或就会还原指针地址。我们就在外面用一下解码函数_objc_decodeTaggedPointer_,拿出string真正的指针地址
image.png
  • 结果显示:61就是a的ASCII码,62就是b的ASCCII码,那么taggedpinter指针包含了值!

再试一下number的地址

image.png

  • 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这个判断条件,

image.png

  • 果然是用来确定类型的,下面验证一下NSDate,是否是这套逻辑,
    image.png

    其中(e:1110)-> 最高位是1,说明是tagged指针,后面三位是6,对照上面的enum,是TAG_NSDate!
由此,我们可以得到taggedPointer的结构

1、指针地址
2、tagged类型的flag
3、值
4、是否是tagged

  • 这样的一个类型,包含了这么多信息,而且是存在常量区,由系统自动管理,读取的效率是相当的。根据官方,是非taggedpointter的3倍,创建的速度比非tagged106倍。
    所以日常开发中,给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> SideTablesMap;

static StripedMap& SideTables() {
    return SideTablesMap.get();
}
  • 是由 StripedMap通过get方法获取

再看一下StripedMap结构

image.png

  • 内存中最多只有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流程分析完毕,下面归纳一下流程
    后补

你可能感兴趣的:(内存管理)