您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“JavaScript內存管理和GC算法實例分析”,內容詳細,步驟清晰,細節處理妥當,希望這篇“JavaScript內存管理和GC算法實例分析”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
JavaScript在創建變量(數組、字符串、對象等)是自動進行了分配內存,并且在不使用它們的時候會“自動”的釋放分配的內容;JavaScript語言不像其他底層語言一樣,例如C語言,他們提供了內存管理的接口,比如malloc()
用于分配所需的內存空間、free()
釋放之前所分配的內存空間。
我們將釋放內存的過程稱為垃圾回收,像JavaScript這種高級語言提供了內存自動分配和自動回收,因為這個自動就導致許多開發者不會去關心內存管理。
即使高級語言提供了自動內存管理,但是我們也需要對內管管理有一下基本的理解,有時候自動內存管理出現了問題,我們可以更好的去解決它,或者說使用代價最小的方法解決它。
其實不管是什么語言,內存的聲明周期大致分為如下幾個階段:
下面我們對每一步進行具體說明:
內存分配:當我們定義變量時,系統會自動為其分配內存,它允許在程序中使用這塊內存。
內存使用:在對變量進行讀寫的時候發生
內存回收:使用完畢后,自動釋放不需要內存,也就是由垃圾回收機制自動回收不再使用的內存
為了保護開發人員的頭發,JavaScript在定義變量時就自動完成了內存分配,示例代碼如下:
let num = 123 // 給數值變量分配內存 let str = '一碗周' // 給字符串分配內存 let obj = { name: '一碗周', age: 18, } // 給對象及其包含的值分配內存 // 給數組及其包含的值分配內存(類似于對象) let arr = [1, null, 'abc'] function fun(a) { return a + 2 } // 給函數(可調用的對象)分配內存 // 函數表達式也能分配一個對象 Element.addEventListener( 'click', event => { console.log(event) }, false, )
有些時候并不會重新分配內存,如下面這段代碼:
// 給數組及其包含的值分配內存(類似于對象) let arr = [1, null, 'abc'] let arr2 = [arr[0], arr[2]] // 這里并不會重新對分配內存,而是直接存儲原來的那份內存
JavaScript中使用值的過程實際上是對分配內存進行讀取與寫入的操作。這里的讀取與寫入可能是寫入一個變量、讀取某個變量的值、寫入一個對象的屬性值以及為函數傳遞參數等。
JavaScript中的內存釋放是自動的,釋放的時機就是某些值(內存地址)不在使用了,JavaScript就會自動釋放其占用的內存。
其實大多數內存管理的問題都在這個階段。在這里最艱難的任務就是找到那些不需要的變量。
雖然說現在打高級語言都有自己垃圾回收機制,雖然現在的垃圾回收算法很多,但是也無法智能的回收所有的極端情況,這就是我們為什么要學習內存管理以及垃圾回收算法的理由了。
接下來我們來討論一下JavaScript中的垃圾回收以及常用的垃圾回收算法。
前面我們也說了,JavaScript中的內存管理是自動的,在創建對象時會自動分配內存,當對象不在被引用或者不能從根上訪問時,就會被當做垃圾給回收掉。
JavaScript中的可達對象簡單的說就是可以訪問到的對象,不管是通過引用還是作用域鏈的方式,只要能訪問到的就稱之為可達對象。可達對象的可達是有一個標準的,就是必須從根上出發是否能被找到;這里的根可以理解為JavaScript中的全局變量對象,在瀏覽器環境中就是window
、在Node環境中就是global
。
為了更好的理解引用的概念,看下面這一段代碼:
let person = { name: '一碗周', } let man = person person = null
圖解如下:
根據上面那個圖我們可以看到,最終這個{ name: '一碗周' }
是不會被當做垃圾給回收掉的,因為還具有一個引用。
現在我們來理解一下可達對象,代碼如下:
function groupObj(obj1, obj2) { obj1.next = obj2 obj2.prev = obj1 return { obj1, obj2, } } let obj = groupObj({ name: '大明' }, { name: '小明' })
調用groupObj()
函數的的結果obj
是一個包含兩個對象的一個對象,其中obj.obj1
的next
屬性指向obj.obj2
;而obj.obj2
的prev
屬性又指向obj.obj2
。最終形成了一個無限套娃。
如下圖:
現在來看下面這段代碼:
delete obj.obj1 delete obj.obj2.prev
我們刪除obj
對象中的obj1
對象的引用和obj.obj2
中的prev
屬性對obj1
的引用。
圖解如下:
此時的obj1
就被當做垃圾給回收了。
GC是Garbage collection的簡寫,也就是垃圾回收。當GC進行工作的時候,它可以找到內存中的垃圾、并釋放和回收空間,回收之后方便我們后續的進行使用。
在GC中的垃圾包括程序中不在需要使用的對象以及程序中不能再訪問到的對象都會被當做垃圾。
GC算法就是工作時查找和回收所遵循的規則,常見的GC算法有如下幾種:
引用計數:通過一個數字來記錄引用次數,通過判斷當前數字是不是0來判斷對象是不是一個垃圾。
標記清除:在工作時為對象添加一個標記來判斷是不是垃圾。
標記整理:與標記清除類似。
分代回收:V8中使用的垃圾回收機制。
引用計數算法的核心思想就是設置一個引用計數器,判斷當前引用數是否為0 ,從而決定當前對象是不是一個垃圾,從而垃圾回收機制開始工作,釋放這塊內存。
引用計數算法的核心就是引用計數器 ,由于引用計數器的存在,也就導致該算法與其他GC算法有所差別。
引用計數器的改變是在引用關系發生改變時就會發生變化,當引用計數器變為0的時候,該對象就會被當做垃圾回收。
現在我們通過一段代碼來看一下:
// { name: '一碗周' } 的引用計數器 + 1 let person = { name: '一碗周', } // 又增加了一個引用,引用計數器 + 1 let man = person // 取消一個引用,引用計數器 - 1 person = null // 取消一個引用,引用計數器 - 1。此時 { name: '一碗周' } 的內存就會被當做垃圾回收 man = null
引用計數算法的優點如下:
發現垃圾時立即回收;
最大限度減少程序暫停,這里因為發現垃圾就立刻回收了,減少了程序因內存爆滿而被迫停止的現象。
缺點如下:
無法回收循環引用的對象;
就比如下面這段代碼:
function fun() { const obj1 = {} const obj2 = {} obj1.next = obj2 obj2.prev = obj1 return '一碗周' } fun()
上面的代碼中,當函數執行完成之后函數體的內容已經是沒有作用的了,但是由于obj1
和obj2
都存在不止1個引用,導致兩種都無法被回收,就造成了空間內存的浪費。
時間開銷大,這是因為引用計數算法需要時刻的去監控引用計數器的變化。
標記清除算法解決了引用計數算法的?些問題, 并且實現較為簡單, 在V8引擎中會有被?量的使?到。
在使?標記清除算法時,未引用對象并不會被立即回收.取?代之的做法是,垃圾對象將?直累計到內存耗盡為?.當內存耗盡時,程序將會被掛起,垃圾回收開始執行.當所有的未引用對象被清理完畢 時,程序才會繼續執行.該算法的核心思想就是將整個垃圾回收操作分為標記和清除兩個階段完成。
第一個階段就是遍歷所有對象,標記所有的可達對象;第二個階段就是遍歷所有對象清除沒有標記的對象,同時會抹掉所有已經標記的對象,便于下次的工作。
為了區分可用對象與垃圾對象,我們需要在每?個對象中記錄對象的狀態。 因此, 我們在每?個對象中加?了?個特殊的布爾類型的域, 叫做marked
。 默認情況下, 對象被創建時處于未標記狀態。 所以, marked
域被初始化為false
。
進行垃圾回收完畢之后,將回收的內存放在空閑鏈表中方便我們后續使用。
標記清除算法最大的優點就是解決了引用計數算法無法回收循環引用的對象的問題 。就比如下面這段代碼:
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()
當函數執行完畢后obj4
的引用并不是0,但是使用引用計數算法并不能將其作為垃圾回收掉,而使用標記清除算法就解決了這個問題。
而標記清除算法的缺點也是有的,這種算法會導致內存碎片化,地址不連續;還有就是使用標記清除算法即使發現了垃圾對象不里能立刻清除,需要到第二次去清除。
標記整理算法可以看做是標記清除算法的增強型,其步驟也是分為標記和清除兩個階段。
但是標記整理算法那的清除階段會先進行整理,移動對象的位置,最后進行清除。
步驟如下圖:
V8是一款主流的JavaScript執行引擎,現在的Node.js和大多數瀏覽器都采用V8作為JavaScript的引擎。V8的編譯功能采用的是及時編譯,也稱為動態翻譯或運行時編譯,是一種執行計算機代碼的方法,這種方法涉及在程序執行過程中(在執行期)而不是在執行之前進行編譯。
V8引擎對內存是設有上限的,在64位操作系統下上限是1.5G的,而32位操作系統的上限是800兆的。至于為什么設置內存上限主要是內容V8引擎主要是為瀏覽器而準備的,不適合太大的空間;還有一點就是這個大小的垃圾回收是很快的,用戶幾乎沒有感覺,從而增加用戶體驗。
V8引擎采用的是分代回收的思想,主要是將我們的內存按照一定的規則分成兩類,一個是新生代存儲區,另一個是老生代存儲區。
新生代的對象為存活時間較短的對象,簡單來說就是新產生的對象,通常只支持一定的容量(64位操作系統32兆、32位操作系統16兆),而老生代的對象為存活事件較長或常駐內存的對象,簡單來說就是經歷過新生代垃圾回收后還存活下來的對象,容量通常比較大。
下圖展示了V8中的內存:
V8引擎會根據不同的對象采用不同的GC算法,V8中常用的GC算法如下:
分代回收
空間復制
標記清除
標記整理
標記增量
上面我們也介紹了,新生代中存放的是存活時間較短的對象。新生代對象回收過程采用的是復制算法和標記整理算法。
復制算法將我們的新生代內存區域劃分為兩個相同大小的空間,我們將當前使用狀態的空間稱之為From狀態,空間狀態的空間稱之為To狀態,
如下圖所示:
我們將活動的對象全部存儲到From空間,當空間接近滿的時候,就會觸發垃圾回收。
首先需要將新生代From空間中的活動對象做標記整理,標記整理完成之后將標記后的活動對象拷貝到To空間并將沒有進行標記的對象進行回收;最后將From空間和To空間進行交換。
還有一點需要說的就是在進行對象拷貝的時候,會出現新生代對象移動至老生代對象中。
這些被移動的對象是具有指定條件的,主要有兩種:
經過一輪垃圾回收還存活的新生代對象會被移動到老生代對象中
在To空間占用率超過了25%,這個對象也會被移動到老生代對象中(25%的原因是怕影響后續內存分配)
如此可知,新生代對象的垃圾回收采用的方案是空間換時間。
老生代區域存放的對象就是存活時間長、占用空間大的對象。也正是因為其存活的時間長且占用空間大,也就導致了不能采用復制算法,如果采用復制算法那就會造成時間長和空間浪費的現象。
老生代對象一般采用標記清除、標記整理和增量標記算法進行垃圾回收。
在清除階段主要才采用標記清除算法來進行回收,當一段時候后,就會產生大量不連續的內存碎片,過多的碎片無法分配足夠的內存時,就會采用標記整理算法來整理我們的空間碎片。
老生代對象的垃圾回收會采用增量標記算法來優化垃圾回收的過程,增量標記算法如下圖所示:
由于JavaScript是單線程,所以程序執行和垃圾回收同時只能運行一個,這就會導致在執行垃圾回收的時候程序卡頓,這樣給用戶的體驗肯定是不好的。
所以提出了增量標記,在程序運行時,程序先跑一段時間,然后進行進行初步的標記,這個標記有可能只標記直接可達的對象,然后程序繼續跑一段時間,在進行增量標記 ,也即是標記哪些間接可達的對象。由此反復,直至結束。
由于JavaScript沒有給我們提供操作內存的API,只能靠本身提供的內存管理,但是我們并不知道實際上的內存管理是什么樣的。而有時我們需要時刻關注內存的使用情況,Performance工具提供了多種監控內存的方式。
首先我們打開Chrome瀏覽器(這里我們使用的是Chrome瀏覽器,其他瀏覽器也是可以的),在地址欄中輸入我們的目標地址,然后打開開發者工具,選擇【性能】面板。
選擇性能面板后開啟錄制功能,然后去訪問具體界面,模仿用戶去執行一些操作,然后停止錄制,最后我們可以在分析界面中分析記錄的內存信息。
內存問題的體現
出現內存的問題主要有如下幾種表現:
頁面出現延遲加載或經常性暫停,它的底層就伴隨著頻繁的垃圾回收的執行;為什么會頻繁的進行垃圾回收,可能是一些代碼直接導致內存爆滿而且需要立刻進行垃圾回收。
關于這個問題我們可以通過內存變化圖進行分析其原因:
頁面持續性出現糟糕的性能表現,也就是說在我們使用的過程中,頁面給我們的感覺就是一直不好用,它的底層我們一般認為都會存在內存膨脹,所謂的內存膨脹就是當前頁面為了達到某種速度從而申請遠大于本身需要的內存,申請的這個存在超過了我們設備本身所能提供的大小,這個時候我們就能感知到一個持續性的糟糕性能的體驗。
導致內存膨脹的問題有可能是我們代碼的問題,也有可能是設備本身就很差勁,想要分析定位并解決的話需要我們在多個設備上進行反復的測試
頁面的性能隨著時間的延長導致頁面越來越差,加載時間越來越長,出現這種問題的原因可能是由于代碼的原因出現內存泄露。
想要檢測內存是否泄漏,我們可以通過內存總視圖來監聽我們的內存,如果內存是持續升高的,就可能已經出現了內存泄露。
在瀏覽器中監控內存主要有以下幾種方式:
瀏覽器提供的任務管理器
Timeline時序圖
堆快照查找分離DOM
判斷是否存在頻繁的垃圾回收
接下來我們就分別講解這幾種方式。
在瀏覽器中按【Shift】+【ESC】鍵即可打開瀏覽器提供的任務管理器,下圖展示了Chrome瀏覽器中的任務管理器,我們來解讀一
上圖中我們可以看到【掘金】標簽頁的【內存占用空間】表示的的這個頁面的DOM在瀏覽器中所占的內存,如果不斷增加就表示有新的DOM在創建;而后面的【JavaScript使用的內存】(默認不開啟,需要通過右鍵開啟)表示的是JavaScript中的堆,而括號中的大小表示JavaScript中的所有可達對象。
上面描述的瀏覽器中提供的任務管理器只能用來幫助我們判斷頁面是否存在問題,卻不能定位頁面的問題。
Timeline是Performance工具中的一個小的選項卡,其中以毫秒為單位記錄了頁面中的情況,從而可以幫助我們更簡單的定位問題。
堆快照是很有針對性的查找當前的界面對象中是否存在一些分離的DOM,分離DOM的存在也就是存在內存泄漏。
首先我們先要弄清楚DOM有幾種狀態:
首先,DOM對象存在DOM樹中,這屬于正常的DOM
然后,不存在DOM樹中且不存在JS引用,這屬于垃圾DOM對象,是需要被回收的
最后,不存在DOM樹中但是存在JS引用,這就是分離DOM,需要我們手動進行釋放。
查找分離DOM的步驟:打開開發者工具→【內存面板】→【用戶配置】→【獲取快照】→在【過濾器】中輸入Detached
來查找分離DOM,
如下圖所示:
查找到創建的分離DOM后,我們找到該DOM的引用,然后進行釋放。
因為GC
工作時應用程序是停止的,如果當前垃圾回收頻繁工作,而且時間過長的話對頁面來說很不友好,會導致應用假死說我狀態,用戶使用中會感知應用有卡頓。
我們可以通過如下方式進行判斷是否存在頻繁的垃圾回收,具體如下:
通過Timeline時序圖判斷,對當前性能面板中的內存走勢進行監控,如果其中頻繁的上升下降,就出現了頻繁的垃圾回收。這個時候要定位代碼,看看是執行什么的時候造成了這種情況。
使用瀏覽器任務管理器會簡單一些,任務管理器中主要是數值的變化,其數據頻繁的瞬間增加減小,也是頻繁的垃圾回收。
讀到這里,這篇“JavaScript內存管理和GC算法實例分析”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。