內存管理
對之前的文章進行重新編輯,內容做了很多的調整,使其具有邏輯更加緊湊,內容更加全面。 1. 基礎概念1.1 生命周期不管什么程序語言,內存生命周期基本是一致的:
在所有語言中第一和第二部分都很清晰。最后一步在低級語言(例如C語言)中很清晰,但是在像JavaScript等高級語言中,這一步依賴于垃圾回收機制,一般情況下不用程序員操心。 1.2 堆與棧我們知道,內存空間可以分為??臻g和堆空間,其中
1.3 基本類型與引用類型在JavaScript中
1.4 V8的變量存放
2. 垃圾回收2.1 分代策略腳本中,絕大多數(shù)對象的生存期很短,只有某些對象的生存期較長。為利用這一特點,V8將堆進行了分代。對象起初會被分配在新生區(qū)。在新生區(qū)的內存分配非常容易:我們只需保有一個指向內存區(qū)的指針,不斷根據(jù)新對象的大小對其進行遞增即可。當該指針達到了新生區(qū)的末尾,就會有一次清理(小周期),清理掉新生區(qū)中不活躍的死對象。對于活躍超過2個小周期的對象,則需將其移動至老生區(qū)。而在老生區(qū)則使用標記清除的算法來進行垃圾回收。V8通過分別對新生代對象和老生代對象使用不同的垃圾回收算法來提升來及回收的效率。這就是所謂的 默認情況下,64位環(huán)境下的V8引擎的新生代內存大小32MB、老生代內存大小為1400MB,而32位則減半,分別為16MB和700MB 根據(jù)
大多數(shù)對象被分配在這里,新生區(qū)是一個很小的區(qū)域,垃圾回收在這個區(qū)域非常頻繁,與其他區(qū)域相獨立。
這里包含大多數(shù)可能存儲指向其他對象的指針的對象,大多數(shù)在新生區(qū)存活了一段時間(2個周期)的對象都會被挪到這里。
這里存放只包含原始數(shù)據(jù)的對象,這些對象沒有執(zhí)行其他對象的指針,例如字符串,數(shù)字數(shù)組等,它們在新生區(qū)存活了一段時間后會被移動到這里。
每一個區(qū)域都是由一組內存頁構成的。除大對象區(qū)的內存頁較大之外,每個區(qū)的內存頁都是1MB大小,且按1MB內存對齊。對象超過一定大小時就會被放置到這個區(qū),垃圾回收期從不移動這個區(qū)域的對象。
代碼對象,也就是包含JIT之后指令的對象,會被分配到這里。這里是唯一擁有執(zhí)行權限的內存區(qū)。(如果代碼對象因過大而被放到大對象區(qū),則該大對象所對應的內存也是可執(zhí)行的。)
這些區(qū)域存放Cell、屬性Cell和Map,每個區(qū)域因為都是存儲相同大小的元素,因此內存結構很簡單,這里也是為了方便進行回收。 在 node-v4.x 之后,區(qū)域進行了合并為:新生區(qū),老生區(qū),大對象區(qū),Map區(qū),Code區(qū) 此外,對于一個對象所占的內存空間,也涉及兩個概念:
這兩個概念在使用chrome的開發(fā)工具中會看到。 垃圾回收釋放的內存即為Retained Size的大小。 2.2 新生區(qū)的半空間分配策略新生代使用半空間(Semi-space)分配策略,其中新對象最初分配在新生代的活躍半空間內。一旦半空間已滿,一個Scavenge操作將活躍對象移出到其他半空間中,被認為是長期駐存的對象,并被晉升為老生代。一旦活躍對象已被移出,則在舊的半空間中剩下的任何死亡對象被丟棄。 具體的如下: YG被平分為兩部分空間From和To,所有內存從To空間被分配出去,當To滿時,開始觸發(fā)GC。 例如說: 某時刻,To已經(jīng)為A、B和C分配了內存,當前它只剩下一小塊內存未分配。而From所有的內存都空閑著。 此時,一個程序需要為D分配內存,但D需要的內存大小超出了To未分配的內存,此時觸發(fā)GC,頁面停止執(zhí)行 接著From和To進行對換,即原來的To空間被標志為From,F(xiàn)rom被標志為To。并且把活的變量值(B)標志出來,而垃圾(A、C)未被標志,它們將會被清掉。 活躍的變量(B)會被復制到To空間,而垃圾(A、C)則被回收。同時,D被分配到To空間,最后的情況如下。 至此,整個GC完成,此過程中頁面會阻塞,所以要盡可能的快。 2.2.1 對象的晉升當一個新生代的對象在滿足一定條件下,會從新生代被移到老生代,這就是對象的晉升。具體的移動的標準有兩種
2.3 老生代V8在老生代中采用Mark-Sweep和Mark-Compact相結合的垃圾回收策略。 2.3.1 標記標記-清除算法分為標記和清除兩個階段。 標記階段,所有堆上的活躍對象都會被標記,每個內存頁有一個用來標記對象的位圖,位圖中的每一位對應的內存頁中的一個字,這個位圖需要占據(jù)一定的空間。另外還有兩位用來標記對象的狀態(tài):
那么這里怎么理解標記的過程?這就必須知道:內存管理方式實際上基于 GC Root是內存的根節(jié)點,在瀏覽器中它是window,在Nodejs中則是global對象
有很多內部的GC Root對用戶來說都不是很重要,從應用的角度來說有下面幾種情況:
實際上,標記的過程正是以由GC Root建立的圖為基礎,來實現(xiàn)對象的標記,標記算法的核心是深度優(yōu)先搜索,大致實現(xiàn)如下:
這個算法實現(xiàn)起來還是蠻繁瑣的,從
標記結束后,所有的對象非黑(活躍節(jié)點)即白(垃圾節(jié)點)。 標記時間取決于必須標記的活躍對象的數(shù)目,對于一個大的web應用,整個堆棧的標記可能需要超過100ms。由于全停頓會造成了瀏覽器一段時間無響應,所以V8使用了一種增量標記的方式標記活躍對象,將完整的標記拆分成很多小的步驟,每做完一部分就停下來,讓JavaScript的應用線程執(zhí)行一會,這樣垃圾回收與應用線程交替執(zhí)行。V8可以讓每個標記步驟的持續(xù)時間低于5ms。 舉個例子:
例如圖中灰色的節(jié)點,它原來代表ob變量值,當 2.3.2 清除(Sweep)由于標記完成后,所有對象都已經(jīng)被標記,即不是活躍對象就是死亡對象,堆上有多少空間已經(jīng)確定。清除時,垃圾回收器會掃描連續(xù)存放的死對象,將其變成空閑空間。這個任務是由專門的清掃線程同步執(zhí)行。 2.3.3 整理(Compact)標記清除有一個問題就是進行一次標記清楚后,內存空間往往是不連續(xù)的,會出現(xiàn)很多的內存碎片。如果后續(xù)需要分配一個需要內存空間較多的對象時,如果所有的內存碎片都不夠用,將會使得V8無法完成這次分配,提前觸發(fā)垃圾回收。 標記整理正是為了解決標記清除所帶來的內存碎片的問題。標記整理在標記清除的基礎進行修改,將其的清除階段變?yōu)榫o縮極端。在整理的過程中,將活著的對象向內存區(qū)的一段移動,移動完成后直接清理掉邊界外的內存。緊縮過程涉及對象的移動,所以效率并不是太好,但是能保證不會生成內存碎片。 2.4 垃圾回收總結
3. 內存問題3.1 內存泄漏內存泄漏是指計算機可用內存的逐漸減少,原因通常是程序持續(xù)無法釋放其使用的臨時內存。 先來一個最簡單的DOM泄漏的例子
程序非常簡單,只是把id為_p的HTML元素從頁面移除,在移除之前從GC Root遍歷此P元素有兩條路可走。在執(zhí)行 3.2 內存占用過多這個問題很容易理解。例如使用事件代理來減少事件監(jiān)聽的函數(shù),從而減少內存分配的開銷。 3.3 gc卡頓如果你的頁面垃圾回收很頻繁,那說明你的頁面可能內存使用分配太頻繁了。頻繁的GC可能也會導致頁面卡頓。 在一些框架中,如果創(chuàng)建一個大對象之后,可能不會很快就將其釋放,而是會緩存起來,直到?jīng)]有用處為止。 4. chrome dev tools在使用Chrome進行內存分析的時候,要先在chrome菜單-》工具,或者直接按shift+esc,找到內存管理器,然后選上JavaScript使用的內存(JavaScipt Memory)。 4.1 timeline通過Timeline的內存模式,可以在宏觀上觀察到web應用的內存情況,一般我們需要關注的點:
這些關注點都可以在timeline的內存視圖中看到,如圖 timeline統(tǒng)計的內存變化主要有:
此外還可以通過 4.2 profile
profile使用必須知道的:
在profile中的幾個概念:
4.2.1 Take Heap Snapshot使用快照,必須知道:
快照有三個視圖,它們分別有各自的作用
4.2.2 Recode Heap Allocations這個功能可以動態(tài)監(jiān)控,通過次工具可以看到
4.3 實踐例子1:timeline來查看正常的內存 例子2:通過timeline來發(fā)現(xiàn)內存泄漏 可以看到隨著時間的增長,頁面占用的內存越來越多, 在這種情況下就可以懷疑有內存泄漏了,也有可能是瀏覽器還沒有進行gc,這個時候我們可以強制進行垃圾回收(垃圾筒圖標) 反復測試,如果發(fā)現(xiàn)無論怎么樣,內存一直在增長,那么估計你就遇到內存泄漏的問題了。 如果頁面中DOM節(jié)點的數(shù)量一直在攀升,那么肯定出現(xiàn)DOM泄漏了 例子3:驗證快照之前會進行gc
例子4:通過snapshot來發(fā)現(xiàn)內存泄漏
可以看到,action之后,內存的數(shù)量是增加的(注意,已經(jīng)gc過了),這說明web應用極有內存泄漏。 一個原則就是找到本不應該存在卻還存在的那些值。 例子5:通過內存分配的情況來分析 點擊藍色的柱子,可以看到詳細的情況,來進行分析 例子6:通過timeline來分析gc過于頻繁導致卡頓的問題 此例子在移動手機的瀏覽器進行測試,頁面還是相對簡單,在比較復雜的移動web應用,這種情況還是比較危險的,可能會導致頁面卡死。 參考 |
|
來自: 郭恩 > 《游戲經(jīng)驗》