深入理解JavaScript內(nèi)存管理和GC算法
前言
JavaScript在創(chuàng)建變量(數(shù)組、字符串、對象等)是自動進行了分配內(nèi)存,并且在不使用它們的時候會“自動”的釋放分配的內(nèi)容;JavaScript語言不像其他底層語言一樣,例如C語言,他們提供了內(nèi)存管理的接口,比如malloc()
用于分配所需的內(nèi)存空間、free()
釋放之前所分配的內(nèi)存空間。
我們將釋放內(nèi)存的過程稱為垃圾回收,像JavaScript這種高級語言提供了內(nèi)存自動分配和自動回收,因為這個自動就導(dǎo)致許多開發(fā)者不會去關(guān)心內(nèi)存管理。
即使高級語言提供了自動內(nèi)存管理,但是我們也需要對內(nèi)管管理有一下基本的理解,有時候自動內(nèi)存管理出現(xiàn)了問題,我們可以更好的去解決它,或者說使用代價最小的方法解決它。
內(nèi)存的生命周期
其實不管是什么語言,內(nèi)存的聲明周期大致分為如下幾個階段:
下面我們對每一步進行具體說明:
- 內(nèi)存分配:當我們定義變量時,系統(tǒng)會自動為其分配內(nèi)存,它允許在程序中使用這塊內(nèi)存。
- 內(nèi)存使用:在對變量進行讀寫的時候發(fā)生
- 內(nèi)存回收:使用完畢后,自動釋放不需要內(nèi)存,也就是由垃圾回收機制自動回收不再使用的內(nèi)存
JavaScript中的內(nèi)存分配
為了保護開發(fā)人員的頭發(fā),JavaScript在定義變量時就自動完成了內(nèi)存分配,示例代碼如下:
let num = 123 // 給數(shù)值變量分配內(nèi)存 let str = '一碗周' // 給字符串分配內(nèi)存 let obj = { name: '一碗周', age: 18, } // 給對象及其包含的值分配內(nèi)存 // 給數(shù)組及其包含的值分配內(nèi)存(類似于對象) let arr = [1, null, 'abc'] function fun(a) { return a + 2 } // 給函數(shù)(可調(diào)用的對象)分配內(nèi)存 // 函數(shù)表達式也能分配一個對象 Element.addEventListener( 'click', event => { console.log(event) }, false, )
有些時候并不會重新分配內(nèi)存,如下面這段代碼:
// 給數(shù)組及其包含的值分配內(nèi)存(類似于對象) let arr = [1, null, 'abc'] let arr2 = [arr[0], arr[2]] // 這里并不會重新對分配內(nèi)存,而是直接存儲原來的那份內(nèi)存
在JavaScript中使用內(nèi)存
JavaScript中使用值的過程實際上是對分配內(nèi)存進行讀取與寫入的操作。這里的讀取與寫入可能是寫入一個變量、讀取某個變量的值、寫入一個對象的屬性值以及為函數(shù)傳遞參數(shù)等。
釋放內(nèi)存
JavaScript中的內(nèi)存釋放是自動的,釋放的時機就是某些值(內(nèi)存地址)不在使用了,JavaScript就會自動釋放其占用的內(nèi)存。
其實大多數(shù)內(nèi)存管理的問題都在這個階段。在這里最艱難的任務(wù)就是找到那些不需要的變量。
雖然說現(xiàn)在打高級語言都有自己垃圾回收機制,雖然現(xiàn)在的垃圾回收算法很多,但是也無法智能的回收所有的極端情況,這就是我們?yōu)槭裁匆獙W(xué)習(xí)內(nèi)存管理以及垃圾回收算法的理由了。
接下來我們來討論一下JavaScript中的垃圾回收以及常用的垃圾回收算法。
JavaScript中的垃圾回收
前面我們也說了,JavaScript中的內(nèi)存管理是自動的,在創(chuàng)建對象時會自動分配內(nèi)存,當對象不在被引用或者不能從根上訪問時,就會被當做垃圾給回收掉。
JavaScript中的可達對象簡單的說就是可以訪問到的對象,不管是通過引用還是作用域鏈的方式,只要能訪問到的就稱之為可達對象。可達對象的可達是有一個標準的,就是必須從根上出發(fā)是否能被找到;這里的根可以理解為JavaScript中的全局變量對象,在瀏覽器環(huán)境中就是window
、在Node環(huán)境中就是global
。
為了更好的理解引用的概念,看下面這一段代碼:
let person = { name: '一碗周', } let man = person person = null
圖解如下:
根據(jù)上面那個圖我們可以看到,最終這個{ name: '一碗周' }
是不會被當做垃圾給回收掉的,因為還具有一個引用。
現(xiàn)在我們來理解一下可達對象,代碼如下:
function groupObj(obj1, obj2) { obj1.next = obj2 obj2.prev = obj1 return { obj1, obj2, } } let obj = groupObj({ name: '大明' }, { name: '小明' })
調(diào)用groupObj()
函數(shù)的的結(jié)果obj
是一個包含兩個對象的一個對象,其中obj.obj1
的next
屬性指向obj.obj2
;而obj.obj2
的prev
屬性又指向obj.obj2
。最終形成了一個無限套娃。
如下圖:
現(xiàn)在來看下面這段代碼:
delete obj.obj1 delete obj.obj2.prev
我們刪除obj
對象中的obj1
對象的引用和obj.obj2
中的prev
屬性對obj1
的引用。
圖解如下:
此時的obj1
就被當做垃圾給回收了。
GC算法
GC是Garbage collection的簡寫,也就是垃圾回收。當GC進行工作的時候,它可以找到內(nèi)存中的垃圾、并釋放和回收空間,回收之后方便我們后續(xù)的進行使用。
在GC中的垃圾包括程序中不在需要使用的對象以及程序中不能再訪問到的對象都會被當做垃圾。
GC算法就是工作時查找和回收所遵循的規(guī)則,常見的GC算法有如下幾種:
- 引用計數(shù):通過一個數(shù)字來記錄引用次數(shù),通過判斷當前數(shù)字是不是0來判斷對象是不是一個垃圾。
- 標記清除:在工作時為對象添加一個標記來判斷是不是垃圾。
- 標記整理:與標記清除類似。
- 分代回收:V8中使用的垃圾回收機制。
引用計數(shù)算法
引用計數(shù)算法的核心思想就是設(shè)置一個引用計數(shù)器,判斷當前引用數(shù)是否為0 ,從而決定當前對象是不是一個垃圾,從而垃圾回收機制開始工作,釋放這塊內(nèi)存。
引用計數(shù)算法的核心就是引用計數(shù)器 ,由于引用計數(shù)器的存在,也就導(dǎo)致該算法與其他GC算法有所差別。
引用計數(shù)器的改變是在引用關(guān)系發(fā)生改變時就會發(fā)生變化,當引用計數(shù)器變?yōu)?的時候,該對象就會被當做垃圾回收。
現(xiàn)在我們通過一段代碼來看一下:
// { name: '一碗周' } 的引用計數(shù)器 + 1 let person = { name: '一碗周', } // 又增加了一個引用,引用計數(shù)器 + 1 let man = person // 取消一個引用,引用計數(shù)器 - 1 person = null // 取消一個引用,引用計數(shù)器 - 1。此時 { name: '一碗周' } 的內(nèi)存就會被當做垃圾回收 man = null
引用計數(shù)算法的優(yōu)點如下:
- 發(fā)現(xiàn)垃圾時立即回收;
- 最大限度減少程序暫停,這里因為發(fā)現(xiàn)垃圾就立刻回收了,減少了程序因內(nèi)存爆滿而被迫停止的現(xiàn)象。
缺點如下:
- 無法回收循環(huán)引用的對象;
就比如下面這段代碼:
function fun() { const obj1 = {} const obj2 = {} obj1.next = obj2 obj2.prev = obj1 return '一碗周' } fun()
上面的代碼中,當函數(shù)執(zhí)行完成之后函數(shù)體的內(nèi)容已經(jīng)是沒有作用的了,但是由于obj1
和obj2
都存在不止1個引用,導(dǎo)致兩種都無法被回收,就造成了空間內(nèi)存的浪費。
- 時間開銷大,這是因為引用計數(shù)算法需要時刻的去監(jiān)控引用計數(shù)器的變化。
標記清除算法
標記清除算法解決了引用計數(shù)算法的?些問題, 并且實現(xiàn)較為簡單, 在V8引擎中會有被?量的使?到。
在使?標記清除算法時,未引用對象并不會被立即回收.取?代之的做法是,垃圾對象將?直累計到內(nèi)存耗盡為?.當內(nèi)存耗盡時,程序?qū)粧炱?垃圾回收開始執(zhí)行.當所有的未引用對象被清理完畢 時,程序才會繼續(xù)執(zhí)行.該算法的核心思想就是將整個垃圾回收操作分為標記和清除兩個階段完成。
第一個階段就是遍歷所有對象,標記所有的可達對象;第二個階段就是遍歷所有對象清除沒有標記的對象,同時會抹掉所有已經(jīng)標記的對象,便于下次的工作。
為了區(qū)分可用對象與垃圾對象,我們需要在每?個對象中記錄對象的狀態(tài)。 因此, 我們在每?個對象中加?了?個特殊的布爾類型的域, 叫做marked
。 默認情況下, 對象被創(chuàng)建時處于未標記狀態(tài)。 所以, marked
域被初始化為false
。
標記清除算法的圖解如下圖所示:
進行垃圾回收完畢之后,將回收的內(nèi)存放在空閑鏈表中方便我們后續(xù)使用。
標記清除算法最大的優(yōu)點就是解決了引用計數(shù)算法無法回收循環(huán)引用的對象的問題 。就比如下面這段代碼:
function fun() { const obj1 = {}, obj2 = {}, obj3 = {}, obj4 = {}, obj5 = {}, obj6 = {} obj1.next = obj2 obj2.next = obj3 obj2.prev = obj6 obj4.next = obj6 obj4.prev = obj1 obj5.next = obj4 obj5.prev = obj6 return obj1 } const obj = fun()
當函數(shù)執(zhí)行完畢后obj4
的引用并不是0,但是使用引用計數(shù)算法并不能將其作為垃圾回收掉,而使用標記清除算法就解決了這個問題。
而標記清除算法的缺點也是有的,這種算法會導(dǎo)致內(nèi)存碎片化,地址不連續(xù);還有就是使用標記清除算法即使發(fā)現(xiàn)了垃圾對象不里能立刻清除,需要到第二次去清除。
標記整理算法
標記整理算法可以看做是標記清除算法的增強型,其步驟也是分為標記和清除兩個階段。
但是標記整理算法那的清除階段會先進行整理,移動對象的位置,最后進行清除。
步驟如下圖:
V8中的內(nèi)存管理
V8是什么
V8是一款主流的JavaScript執(zhí)行引擎,現(xiàn)在的Node.js和大多數(shù)瀏覽器都采用V8作為JavaScript的引擎。V8的編譯功能采用的是及時編譯,也稱為動態(tài)翻譯或運行時編譯,是一種執(zhí)行計算機代碼的方法,這種方法涉及在程序執(zhí)行過程中(在執(zhí)行期)而不是在執(zhí)行之前進行編譯。
V8引擎對內(nèi)存是設(shè)有上限的,在64位操作系統(tǒng)下上限是1.5G的,而32位操作系統(tǒng)的上限是800兆的。至于為什么設(shè)置內(nèi)存上限主要是內(nèi)容V8引擎主要是為瀏覽器而準備的,不適合太大的空間;還有一點就是這個大小的垃圾回收是很快的,用戶幾乎沒有感覺,從而增加用戶體驗。
V8垃圾回收策略
V8引擎采用的是分代回收的思想,主要是將我們的內(nèi)存按照一定的規(guī)則分成兩類,一個是新生代存儲區(qū),另一個是老生代存儲區(qū)。
新生代的對象為存活時間較短的對象,簡單來說就是新產(chǎn)生的對象,通常只支持一定的容量(64位操作系統(tǒng)32兆、32位操作系統(tǒng)16兆),而老生代的對象為存活事件較長或常駐內(nèi)存的對象,簡單來說就是經(jīng)歷過新生代垃圾回收后還存活下來的對象,容量通常比較大。
下圖展示了V8中的內(nèi)存:
V8引擎會根據(jù)不同的對象采用不同的GC算法,V8中常用的GC算法如下:
- 分代回收
- 空間復(fù)制
- 標記清除
- 標記整理
- 標記增量
新生代對象垃圾回收
上面我們也介紹了,新生代中存放的是存活時間較短的對象。新生代對象回收過程采用的是復(fù)制算法和標記整理算法。
復(fù)制算法將我們的新生代內(nèi)存區(qū)域劃分為兩個相同大小的空間,我們將當前使用狀態(tài)的空間稱之為From狀態(tài),空間狀態(tài)的空間稱之為To狀態(tài),
如下圖所示:
我們將活動的對象全部存儲到From空間,當空間接近滿的時候,就會觸發(fā)垃圾回收。
首先需要將新生代From空間中的活動對象做標記整理,標記整理完成之后將標記后的活動對象拷貝到To空間并將沒有進行標記的對象進行回收;最后將From空間和To空間進行交換。
還有一點需要說的就是在進行對象拷貝的時候,會出現(xiàn)新生代對象移動至老生代對象中。
這些被移動的對象是具有指定條件的,主要有兩種:
- 經(jīng)過一輪垃圾回收還存活的新生代對象會被移動到老生代對象中
- 在To空間占用率超過了25%,這個對象也會被移動到老生代對象中(25%的原因是怕影響后續(xù)內(nèi)存分配)
如此可知,新生代對象的垃圾回收采用的方案是空間換時間。
老生代對象垃圾回收
老生代區(qū)域存放的對象就是存活時間長、占用空間大的對象。也正是因為其存活的時間長且占用空間大,也就導(dǎo)致了不能采用復(fù)制算法,如果采用復(fù)制算法那就會造成時間長和空間浪費的現(xiàn)象。
老生代對象一般采用標記清除、標記整理和增量標記算法進行垃圾回收。
在清除階段主要才采用標記清除算法來進行回收,當一段時候后,就會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,過多的碎片無法分配足夠的內(nèi)存時,就會采用標記整理算法來整理我們的空間碎片。
老生代對象的垃圾回收會采用增量標記算法來優(yōu)化垃圾回收的過程,增量標記算法如下圖所示:
由于JavaScript是單線程,所以程序執(zhí)行和垃圾回收同時只能運行一個,這就會導(dǎo)致在執(zhí)行垃圾回收的時候程序卡頓,這樣給用戶的體驗肯定是不好的。
所以提出了增量標記,在程序運行時,程序先跑一段時間,然后進行進行初步的標記,這個標記有可能只標記直接可達的對象,然后程序繼續(xù)跑一段時間,在進行增量標記 ,也即是標記哪些間接可達的對象。由此反復(fù),直至結(jié)束。
Performance工具
由于JavaScript沒有給我們提供操作內(nèi)存的API,只能靠本身提供的內(nèi)存管理,但是我們并不知道實際上的內(nèi)存管理是什么樣的。而有時我們需要時刻關(guān)注內(nèi)存的使用情況,Performance工具提供了多種監(jiān)控內(nèi)存的方式。
Performance使用步驟
首先我們打開Chrome瀏覽器(這里我們使用的是Chrome瀏覽器,其他瀏覽器也是可以的),在地址欄中輸入我們的目標地址,然后打開開發(fā)者工具,選擇【性能】面板。
選擇性能面板后開啟錄制功能,然后去訪問具體界面,模仿用戶去執(zhí)行一些操作,然后停止錄制,最后我們可以在分析界面中分析記錄的內(nèi)存信息。
結(jié)果如下圖所示:
內(nèi)存問題的體現(xiàn)
出現(xiàn)內(nèi)存的問題主要有如下幾種表現(xiàn):
- 頁面出現(xiàn)延遲加載或經(jīng)常性暫停,它的底層就伴隨著頻繁的垃圾回收的執(zhí)行;為什么會頻繁的進行垃圾回收,可能是一些代碼直接導(dǎo)致內(nèi)存爆滿而且需要立刻進行垃圾回收。
關(guān)于這個問題我們可以通過內(nèi)存變化圖進行分析其原因:
- 頁面持續(xù)性出現(xiàn)糟糕的性能表現(xiàn),也就是說在我們使用的過程中,頁面給我們的感覺就是一直不好用,它的底層我們一般認為都會存在內(nèi)存膨脹,所謂的內(nèi)存膨脹就是當前頁面為了達到某種速度從而申請遠大于本身需要的內(nèi)存,申請的這個存在超過了我們設(shè)備本身所能提供的大小,這個時候我們就能感知到一個持續(xù)性的糟糕性能的體驗。
導(dǎo)致內(nèi)存膨脹的問題有可能是我們代碼的問題,也有可能是設(shè)備本身就很差勁,想要分析定位并解決的話需要我們在多個設(shè)備上進行反復(fù)的測試
- 頁面的性能隨著時間的延長導(dǎo)致頁面越來越差,加載時間越來越長,出現(xiàn)這種問題的原因可能是由于代碼的原因出現(xiàn)內(nèi)存泄露。
想要檢測內(nèi)存是否泄漏,我們可以通過內(nèi)存總視圖來監(jiān)聽我們的內(nèi)存,如果內(nèi)存是持續(xù)升高的,就可能已經(jīng)出現(xiàn)了內(nèi)存泄露。
監(jiān)控內(nèi)存的方式
在瀏覽器中監(jiān)控內(nèi)存主要有以下幾種方式:
- 瀏覽器提供的任務(wù)管理器
- Timeline時序圖
- 堆快照查找分離DOM
- 判斷是否存在頻繁的垃圾回收
接下來我們就分別講解這幾種方式。
任務(wù)管理器監(jiān)控內(nèi)存
在瀏覽器中按【Shift】+【ESC】鍵即可打開瀏覽器提供的任務(wù)管理器,下圖展示了Chrome瀏覽器中的任務(wù)管理器,我們來解讀一下
上圖中我們可以看到【掘金】標簽頁的【內(nèi)存占用空間】表示的的這個頁面的DOM在瀏覽器中所占的內(nèi)存,如果不斷增加就表示有新的DOM在創(chuàng)建;而后面的【JavaScript使用的內(nèi)存】(默認不開啟,需要通過右鍵開啟)表示的是JavaScript中的堆,而括號中的大小表示JavaScript中的所有可達對象。
Timeline記錄內(nèi)存
上面描述的瀏覽器中提供的任務(wù)管理器只能用來幫助我們判斷頁面是否存在問題,卻不能定位頁面的問題。
Timeline是Performance工具中的一個小的選項卡,其中以毫秒為單位記錄了頁面中的情況,從而可以幫助我們更簡單的定位問題。
堆快照查找分離DOM
堆快照是很有針對性的查找當前的界面對象中是否存在一些分離的DOM,分離DOM的存在也就是存在內(nèi)存泄漏。
首先我們先要弄清楚DOM有幾種狀態(tài):
- 首先,DOM對象存在DOM樹中,這屬于正常的DOM
- 然后,不存在DOM樹中且不存在JS引用,這屬于垃圾DOM對象,是需要被回收的
- 最后,不存在DOM樹中但是存在JS引用,這就是分離DOM,需要我們手動進行釋放。
查找分離DOM的步驟:打開開發(fā)者工具→【內(nèi)存面板】→【用戶配置】→【獲取快照】→在【過濾器】中輸入Detached
來查找分離DOM,
如下圖所示:
查找到創(chuàng)建的分離DOM后,我們找到該DOM的引用,然后進行釋放。
判斷是否存在頻繁的垃圾回收
因為GC
工作時應(yīng)用程序是停止的,如果當前垃圾回收頻繁工作,而且時間過長的話對頁面來說很不友好,會導(dǎo)致應(yīng)用假死說我狀態(tài),用戶使用中會感知應(yīng)用有卡頓。
我們可以通過如下方式進行判斷是否存在頻繁的垃圾回收,具體如下:
- 通過Timeline時序圖判斷,對當前性能面板中的內(nèi)存走勢進行監(jiān)控,如果其中頻繁的上升下降,就出現(xiàn)了頻繁的垃圾回收。這個時候要定位代碼,看看是執(zhí)行什么的時候造成了這種情況。
- 使用瀏覽器任務(wù)管理器會簡單一些,任務(wù)管理器中主要是數(shù)值的變化,其數(shù)據(jù)頻繁的瞬間增加減小,也是頻繁的垃圾回收。
寫在最后
本篇文章介紹了JavaScript的垃圾回收機制以及常用的垃圾回收算法;還講解了V8引擎中的內(nèi)存管理,最后介紹了Performance工具如何使用。
到此這篇關(guān)于深入理解JavaScript內(nèi)存管理和GC算法的文章就介紹到這了,更多相關(guān)JS內(nèi)存管理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Bootstrap編寫一個兼容主流瀏覽器的受眾巨幕式風格頁面
這篇文章主要介紹了Bootstrap編寫一個兼容IE8、谷歌等主流瀏覽器的受眾巨幕式風格頁面,感興趣的小伙伴們可以參考一下2016-07-07JavaScript數(shù)據(jù)結(jié)構(gòu)之雙向鏈表
這篇文章主要為大家詳細介紹了JavaScript數(shù)據(jù)結(jié)構(gòu)之雙向鏈表,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-03-03JS實現(xiàn)線性表的順序表示方法示例【經(jīng)典數(shù)據(jù)結(jié)構(gòu)】
這篇文章主要介紹了JS實現(xiàn)線性表的順序表示方法,簡單分析了線性表的原理并結(jié)合實例形式給出了線性表的插入與刪除實現(xiàn)技巧,需要的朋友可以參考下2017-04-04用js實現(xiàn)的自定義的對話框的實現(xiàn)代碼
javascript alert函數(shù)的替代方案,一個自定義的對話框的方法2010-03-03多種方法實現(xiàn)load加載完成后把圖片一次性顯示出來
如何一個load 加載完成后把圖片一次性顯示出來,下面有個不錯的方法,希望對大家有所幫助2014-02-02layui點擊數(shù)據(jù)表格添加或刪除一行的例子
今天小編就為大家分享一篇layui點擊數(shù)據(jù)表格添加或刪除一行的例子,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09JavaScript 異步調(diào)用框架 (Part 4 - 鏈式調(diào)用)
我們已經(jīng)實現(xiàn)了一個簡單的異步調(diào)用框架,然而還有一些美中不足,那就是順序執(zhí)行的異步函數(shù)需要用嵌套的方式來聲明。2009-08-08