ObjectiveC和JS的內存管理區別

入職后從iOS轉向了ReactNative,也寫了不少ReactNative需求,最近突然和同事聊到ReactNative 內存管理,發現自己對這塊還不太了解,為此調研了ReactNative內存管理知識,與iOS 中Objective做為對比總結
文章中關于ObjectiveC的內存管理知識,來自蘋果最新源碼。關于JS相關知識,來自網絡調研和JavaScript高級程序設計一書,若有不同看法或發現錯誤,歡迎拍磚指正

參考:

objc4-866源碼
objc4歷史版本
WWDC Advancements in the Objective-C runtime

總結

  1. ObjectiveC主要采樣引用計數管理內存,引用技術存儲在isa_t的extra_rc和散列表的引用計數表里
  2. ObjectiveC的TaggedPointer的值存儲在指針中,存儲在棧上,不需要通過引用計數管理內存
  3. JS主要通過標記的方式管理內存, 在作用域的變量會加標記。垃圾回收程序每次運行時會清理未使用的變量
  4. 引用技術的方式容易造成循環引用,標記清除的方式更容易造成內存泄露

1. 內存區

在iOS中,主要把內存分為五大區,從高到底分別為

  • 棧區 存放函數變量和函數參數
  • 堆區 存放動態分配的內存段
  • 全局靜態區 存放全局變量、靜態變量
  • 常量區 存放常量
  • 代碼區 存放程序代碼
    35edb41f49616b06502653de4cd9713a (1).png

2. ObjectiveC 內存管理

2.1 內存管理方式

ObjectiveC的內存管理主要分為兩種方式,即MRC(手動管理)和ARC(自動),都是引用計數的方式管理內存,區別在于ARC模式下,編譯器會自動的幫程序要添加引用計數+1和-1代碼

2.2 兩種內存管理方案

2.2.1 非引用計數管理(TaggedPointer對象)

總結:TaggedPointer對象的值存儲在指針中,指針存儲在棧上,無需引用計數管理, 開發者也無需管理其內存

蘋果從32位轉向64位時,**NSString**、**NSNumer**、**NSDate**這類型數據,如果用舊的方式管理,會造成資源和效率的浪費,畢竟一個簡短的字符串如果定義為一個對象,存儲isa。class等相關的信息會造成不必要的空間資源浪費,而為管理起引用計數、生命周期,也會造成時間效率上的浪費。因此蘋果定義了一種新對象taggedPointer,為了對此做出改進
taggedpointer的改進在于,指針中存儲了taggedpointer對象的值,除此之外,指針中部分空間存儲標記,如:是否是taggedPoninter對象、是什么類型的**taggedPointer對象(NSString/NSNmer/NSDate)
他的數據結構如下:

d6f280d657940f12c4bc88014d32be3b.png

objc4-866源碼 源碼中看,判斷是否是taggedPointer對象,拿著
指針與_OBJC_TAG_MASK做了操作,如果結果仍然是_OBJC_TAG_MASK,則判斷為taggedPointer對象

_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

_OBJC_TAG_MASK定義可看出, 編譯器在不同的64位上通過判斷最高位或最低位是否是為1,來判斷是否是taggedPointer對象

#if OBJC_SPLIT_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#elif OBJC_MSB_TAGGED_POINTERS
#   define _OBJC_TAG_MASK (1UL<<63)
#else
#   define _OBJC_TAG_MASK 1UL
#endif

TaggedPointer生命周期管理
TaggedPointer對象的值存儲在指針中,指針又存儲在棧上,所以不需要引用計數管理, 這里retain方法在判斷是isTaggedPointer時,直接return,什么都不做

objc_object::retain()
{
    ASSERT(!isTaggedPointer());
    if (fastpath(!ISA()->hasCustomRR())) {
        return sidetable_retain();
    }
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
}

TaggedPointer釋放
追relase源碼調用,可看到最后調用到了objc_object::rootRelease()這個方法,方法內部先判斷如果是TaggedPointer對象的話,return什么都不做

-(void) release
{
    _objc_rootRelease(self);
}
_objc_rootRelease(id obj)
{
    ASSERT(obj);

    obj->rootRelease();
}
objc_object::rootRelease()
{
    return rootRelease(true, RRVariant::Fast);
}

// Base release implementation, ignoring overrides.
// Does not call -dealloc.
// Returns true if the object should now be deallocated.
// This does not check isa.fast_rr; if there is an RR override then 
// it was already called and it chose to call [super release].
inline bool 
objc_object::rootRelease()
{
    if (isTaggedPointer()) return false;
    return sidetable_release();
}

2.2.2 引用計數管理(object對象)

談到iOS,離不開面向對象這個概念。對象什么時候創建、什么時候該釋放,都是用引用計數來管理的。

  1. 當對象創建時引用計數=0,被引用時調用-(void)retain方法,將其引用計數為+1。
  2. 解除引用時調用-(void)release對引用計數-1。
  3. 當引用計數減為0時,表示對象不再使用,此時會釋放對象所占用的堆空間, 并調用- (void)dealloc析構方法

那么,引用技術存儲在哪呢?堆、棧還是其他地方? 又是怎么與對象關聯的呢?
結論:
nonapointer_isa對象,存儲在extra_rc和SideTables中,
pointer_isa,即純指針類型的isa,存儲在SideTables中

他倆的區別在于,isa是指針還是isa_t聯合體
如何證明上邊結論?從前邊的release方法跟下去,最終源碼會走到這個方法,這里做的操作為

  1. 如果是pointer_isa對象,直接查找全局散列表,招到對應的引用計數表,再從引用計數表里,將當前對象的引用計數-1
  2. 如果是nonapointer_isa對象
    a. 先將isa_t里的extra_rc-1
    b. 當extra_rc=0,從散列表中取出一半的引用計數值,做-1操作后賦值給extra_rc
    這里有個問題, 那就是上邊的ab流程, 蘋果為什么這么設計,主要是考慮從extra_rc里操作引用計數,是直接對聯合體isa_t的地址做與操作,比從散列表里查詢、取值、操作效率更快。
    這里源碼加了注釋,直接看源碼就可以了
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) {
        // These checks are only meaningful for objc_release()
        // They are here so that we avoid a re-load of the isa.
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa().bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa().bits);
            return false;
        }
    }
retry:
    do {
        newisa = oldisa;
        // 判斷如果是指針isa,則調用sidetable_release從全局散列表查詢當前對象的引用計數并-1
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa().bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        // 如果當前對象正在釋放析構,則直接return
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa().bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }
        //走到這里說明是nonpointer isa, 這里的代碼主要做的操作就是清楚isa里的extra_rc--
        //如果extra_rc--減為0,則跳轉到underflow:
        // 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;

    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;

    // underflow主要做的操作就是對散列表里當前對象的引用計數-1
 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 (variant != RRVariant::Full) {
            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.
            oldisa = LoadExclusive(&isa().bits);
            goto retry;
        }

        // 這里蘋果注釋很明白了,嘗試對引用計數表里的引用計數-1
        // Try to remove some retain counts from the side table.        
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there

        // 讓后將引用計數表里的部分值,移到extra_rc中
        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            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 && oldisa.nonpointer) {
                // 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.
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }

            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.
        }
    }
    // 當extra_rc和引用計數表里的引用計數=0時,釋放對象,執行析構函數
deallocate:
    // Really deallocate.

    ASSERT(newisa.isDeallocating());
    ASSERT(isa().isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        this->performDealloc();
    }
    return true;
}

這里是對散列表里引用計數的操作: 從散列表里取引用計數的一般,-1后賦值給extra_rc。

// Move some retain counts from the side table to the isa field.
// Returns the actual count subtracted, which may be less than the request.
objc_object::SidetableBorrow
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 };
}

2.3. iOS開發 內存注意事項

3. JS內存管理

總結
JavaScript是使用垃圾回收的編程語言,開發者不需要操心內存分配和回收。JavaScript的垃圾回收規則為:
1. 離開作用域的值會被自動標記為可回收,然后在垃圾回收期間被刪除
2. 主流的垃圾回收算法是標記清理,即先給當前不使用的值加上標記,再回來回收他們的內存
3. 引用計數是另一種垃圾回收策略,需要記錄值被引用了多少次。JavaScript引擎不再使用這種算法。在某些舊版本的IE仍然會受這種算法的影響,是因為JavaScript會訪問非原生JavaScript對象(如DOM元素)
4. 引用計數在代碼中存在循環引用會出現內存泄露
5. 解除變量的引用可以消除循環引用,而且對垃圾回收也有幫助。為促進垃圾回收,全局對象、全局對象的屬性和循環引用都應該在不需要時接觸引用

3.1 引用計數管理

在早期的JS中,會使用引用計數管理內存,和iOS類似,每個值都會記錄它被引用的次數。被引用時,引用數+1,引用解除時,引用數-1。垃圾回收程序會在每次運行的時候釋放引用數=0的內存

3.2 標記清理

目前JS主要用這種方式管理內存。
當變量進入上下文,比如在函數內部聲明一個變量時,這個變量會加上存在與上下文中的標記。當變量離開上下文時,也會被加上離開上下文的標記。
垃圾回收程序運行的時候
1. 會標記內存中存儲的所有變量
2. 將所有在上下文中的變量、被在上下文中的變量引用的變量的標記去掉
3. 在此之后再被加上標記的變量就是待刪除的變量。是因為沒有上下文或變量訪問這些被標記的變量。此時垃圾回收程序會做一次內存清理
2008年后,主流瀏覽器都在自己的JavaScript實現中采用標記清理

原始值和引用值

JS變量可以保持兩種類型的值:原始值和引用值。原始值有:UndefinedNull、Boolen、Number、StringSymbol。區別如下

  1. 原始值大小固定,保存在棧上
  2. 引用值是對象,存儲在堆上
  3. 將一個變量的原始值賦值給另一個變量的原始值,會執行深拷貝
  4. 包含引用值的變量實際上包含的是響應對象的指針,并不是對象本身
  5. typeof 用于確定值的原始類型,instanceof用于確定值的引用類型

3.1 JS開發,內存注意事項

  1. 通過const和let聲明來提高性能。是因為const和let都以塊為作用域,因此相較于var,前兩者更容易被垃圾回收程序回收釋放內存
  2. 注意內存泄露。意外聲明全局變量可能導致內存泄露,如
        function setName() {
        name = '本地生活666';
        }

此時,編譯器會把變量當做window的屬性來創建(相當于window.name='本地生活666')。在window上創建的屬性,只要window存在,name就不會消失。解決方案也很簡單,就是在聲明變量name的時候加上var、letconst關鍵字

  1. 定時器可能會導致內存泄露
let name = '本地生活'
setInteral(() => {
    console.log(name);
}, 100)

只要定時器一直運行,回調函數中引用的name就會一致占用內存

  1. 使用JS閉包造成內存泄露
let outer = function() {
    let name = '本地生活';
    return function() {
        return name;
        };
};

調用outer()會導致分配給name的內存被泄露

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容

  • iOS中內存管理機制是開發中一項很重要的知識,了解iOS中內存管理的規則不管是在開發中還是在學習中都能很大程度的幫...
    Horson19閱讀 1,210評論 0 4
  • 內存五大區 內存布局 當程序運行時,系統會開辟三個區,分別是:內核區、程序使用的內存五大區和保留區。操作系統分為兩...
    淺墨入畫閱讀 254評論 0 1
  • iOS中內存管理機制是開發中一項很重要的知識,了解iOS中內存管理的規則不管是在開發中還是在學習中都能很大程度的幫...
    Horson19閱讀 1,952評論 0 7
  • 一、在 Obj-C 中,如何檢測內存泄漏?你知道哪些方式? 目前我知道的方式有以下幾種 Memory Leaks ...
    灰溜溜的小王子閱讀 637評論 0 1
  • 1、內存布局 stack:方法調用 heap:通過alloc等分配對象 bss:未初始化的全局變量等。 data:...
    AKyS佐毅閱讀 1,610評論 0 19