美文网首页
内存管理

内存管理

作者: Rachel_雷蕾 | 来源:发表于2020-11-27 20:05 被阅读0次

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

    一、内存分布

    内存主要分为五大区,按照地址从高向低依次为:栈区 -> 堆区 -> 全局区 -> 常量区 -> 代码区(__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<StripedMap<SideTable>> SideTablesMap;
    
    static StripedMap<SideTable>& 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流程分析完毕,下面归纳一下流程
      后补

    相关文章

      网友评论

          本文标题:内存管理

          本文链接:https://www.haomeiwen.com/subject/lccniktx.html