90%以上前端開發(fā)人員忽略的問題,確是很多大廠HR問到的題目

「閱讀本文,可以了解到JS中是如何進行垃圾回收的,為什么需要垃圾回收,在這個基礎上,作為開發(fā)者我們可以如何優(yōu)化我們的應用」

一、垃圾回收

首先我們需要了解一下棧內存和堆內存的區(qū)別,棧內存一般是由操作系統(tǒng)去自動管理的,而下面我們要討論的內存管理指的是堆內存,可以被人工管理,比如c/c++,但是人工的風險總是很大的,所以很多語言引入了自動堆內存管理GC機制,比如JVM、JavaScript、C#、Golang、OCaml和ruby。但是交給這個語言機制就真的萬無一失了么,你錯了,因為GC也是程序員創(chuàng)作的,那就會有BUG~

垃圾回收(GC: Garbage Collecation)垃圾回收器特定的時候某種策略去找到程序中不再使用的變量,然后釋放它所占用的內存

想必我們也會產(chǎn)生下面的疑問:

  1. 垃圾回收器是什么?
  2. 特定的時候指的是什么時候?
  3. 某種策略又有哪些?
  4. 如何判斷不再使用

接下來我們帶著這些疑問接著往下看。

二、垃圾回收策略

為什么還需要講究策略,我們知道JavaScript是單線程語言,所有變量的狀態(tài)都受該線程管理,而垃圾回收器做的事情是改變這些變量的狀態(tài),也就是說當其工作的時候,為了拿到變量的最新狀態(tài),JavaScript的主線程是處于等待狀態(tài)的,意味著回收動作會阻塞代碼的執(zhí)行,那么回收策略就顯得至關重要了。

1. 標記清除/整理(緊縮)法

結合我們日常編碼,我們每次聲明變量的時候一般會給變量賦值,當執(zhí)行到這里的時候,系統(tǒng)會給上面的值類型的值以及引用類型的引用和值進行內存的分配,而此時如果內存不足以寫入新的內容,那么就會執(zhí)行垃圾回收,具體的分為標記清除/整理兩個階段:

  • 標記:從根(這個是可達性算法的關鍵,到底什么是根,并不是我們平時理解的window/document等全局變量,可以理解成垃圾回收器的某個實例)使用深度優(yōu)先搜索,將可達的對象打上活躍的標記。
  • 清除:將標記階段不活躍的變量占用內存收回
  • 整理(緊縮)::將標記為活躍的變量向內存的一端移動,另一端不活躍的則進行釋放 清除整理的區(qū)別在于前者會產(chǎn)生不連續(xù)的內存碎片,影響下次新的寫入,后者的話可以理解通過移動重新分配新的內存區(qū)域給活躍的變量,這樣整個內存區(qū)域則都是連續(xù)的,方便下次寫入,兩者都會將不活躍的變量占用內存進行釋放回收。

上面提到,回收會阻塞主線程代碼的執(zhí)行,如果說為了處理很多活躍對象的回收時,可能會引發(fā)長時間的停頓,為了解決這個問題,谷歌在原來的基礎上引入了增量標記惰性清理的方法,大大減少了停頓時間。

2. 引用計數(shù)法

區(qū)別于上文給活躍的變量打標記的做法,引用計數(shù)法將是否活躍的判定更加的簡單化,也就是去檢查改變量被引用的次數(shù),一旦不被任何對象引用,則可進行釋放回收。引用計數(shù)的話有一個明顯的問題:循環(huán)引用造成的內存泄漏,比較常見于兩個互相引用的對象,其中一個執(zhí)行JSON.stringify的時候,會觸發(fā)這個隱患,目前比較簡單的解決方案是將引用的對象暫存在其他變量上,然后通過賦值斷開對象間的互相引用。 上面的可達性算法之所以不會有這個問題,就在于根的定義,即使出現(xiàn)循環(huán)引用,但是對象與根的連接已經(jīng)斷開,于是被判定為可回收。

3. 分代算法

腳本中,絕大多數(shù)對象的生存期很短,只有某些對象的生存期較長。為利用這一特點,V8將堆內存分了新生代和老生代區(qū)域,當然還有其他區(qū)域,這里不詳細說:

  • 新生代:大多數(shù)對象被分配在這里。新生區(qū)是一個很小的區(qū)域,垃圾回收在這個區(qū)域非常頻繁,與其他區(qū)域相獨立,因為回收比較活躍,所以存儲的是存活較短的對象
  • 老生代:扛過新生期沒被回收的大佬,就會轉移到這個區(qū)域 在分代的基礎上,新生代內存區(qū)域采用的回收方法主要是Scavenge-Cheney算法,將該區(qū)域的變量分為FromTo兩個子區(qū)域,每個變量寫入時都會首先在From區(qū)域分配內存,當新生代區(qū)域內存滿了的時候,會檢查From中不活躍的對象,將其釋放,然后將剩下的活躍對象復制到To區(qū)域,再進行交換,也就是原來的活躍對象又換到From區(qū)域,不斷進行周期性上述動作,最后將度過超過2個周期的對象移動到老生代區(qū)域。 而老生代區(qū)域采取的是上面介紹的標記-清除/整理方法去進行內存回收,這里就不多做介紹了。

三、內存管理中產(chǎn)生的問題

通過上面我們知道了再JavaScript中,內存的管理是托管給垃圾回收程序通過不同的方法和策略去執(zhí)行的,同樣的并沒有完美無缺的方法,是存在漏洞的,比如上面提到的循環(huán)引用所導致的內存問題,那么接下來我們可以了解下關于內存管理中常見的有哪些問題,以及他們的表現(xiàn)是什么

1. 內存泄漏

在我們訪問頁面的時候,如果隨著時間的推移,頁面的性能會逐漸變差,給人越來越卡的感覺,那么這就很有可能是由于頁面中異常使用越來越多的內存導致的內存泄漏,代碼中比較常見造成這個現(xiàn)象的有下面幾種情況:

過度使用緩存

開發(fā)的過程中,使用大量的對象去緩存數(shù)據(jù),并且沒在不被需要的時候及時清理掉,這就導致無法被垃圾回收器回收掉,造成內存浪費

不合理的使用閉包

眾所周知,閉包的隱患之一就是內部函數(shù)外部引用的變量無法隨著內部函數(shù)作用域的消失而釋放,這也會導致內存泄漏的問題

無效的DOM引用

我們經(jīng)常會將DOM對象保存下來,但是卻總是忘記在銷毀DOM后或者不需要該引用的時候,去釋放對應的DOM引用變量

未清除的定時器及未解綁的全局監(jiān)聽事件

如果沒有及時的將不再使用的定時器或者監(jiān)聽事件程序消除,那么它們將會一直存在在內存中,造成泄漏

2. 內存膨脹

內存膨脹的表現(xiàn)是用戶進入應用,給人的表現(xiàn)一直很差,不是很流暢,也就是頁面使用的內存超過了該終端的頁面最佳速度所需的內存。 當然,這個也是根據(jù)不同設備有不同的結果,那么又是如何去判定呢?這邊有一個RAIL模型可以用來測試你的頁面體驗情況,從而判斷是否存在內存膨脹的問題

3. 垃圾回收頻繁觸發(fā)

通過上面的內容我們知道,當我們每次往內存寫入新的變量的時候,就有可能觸發(fā)垃圾回收,而垃圾回收的話可能造成停頓,所以說頻繁過多的執(zhí)行對一些實時性比較高的場景(比如游戲、動畫)是不太友好的。那么在這種情況下,理論上我們減少新變量的寫入,復用之前的舊變量就有一定的效果了,下面我們詳細看看

對象Object的優(yōu)化

在日常的開發(fā)當中,我們經(jīng)常這么做:

function a(){
  //do something
  return {
    name:'leon'
  }
}
//模擬一個多次執(zhí)行的情況
document.body.onclick = function(){
  this.res = a()
}
復制代碼

通過調用一個返回新對象結果的方法,獲取最新的某個值,最后更新上去,上面的click每執(zhí)行一次,方法a便需要去內存里面申請一個空間給返回值使用,等到下次回收的時候再去釋放這個空間,那么這個時候,如果我們優(yōu)化一下,就能避免了

function a(){
  //do something
  this.res.name = 'leon'
}

//模擬一個多次執(zhí)行的情況
document.body.onclick = function(){
  a()
}
復制代碼

這樣的話就能避免新的對象寫入,實時性要求很高的代碼中,將會有效的減少垃圾堆積,并且最終避免垃圾回收停頓,具有一定收益。

數(shù)組Array的優(yōu)化

大家應該對下面的代碼很熟悉,我們會聲明一個容器去根據(jù)不同的條件塞進去不同值,然后最后收集每次值

var a = []
var b = []
for(var i = 0; i < 10; i++){
    b = []
    //do something
    for(var j = 0; j < 5; j++>){
      b.push(j)
    }
    a.push(b)
}
復制代碼

顯而易見,每次外循環(huán)我們都需要去重置原先的容器,好push新的值進去,這個時候我們就會往內存中寫入一個新的Array類型的內容,等到下次循環(huán)的時候,失去引用之后,便被回收。那我們有沒有一種既可以清空容器又不會新寫一個內容的操作呢,請往下看

var a = []
var b = []
for(var i = 0; i < 10; i++){
    b.length = 0
    //do something
    for(var j = 0; j < 5; j++>){
      b.push(j)
    }
    a.push(b)
}
復制代碼

實際上,將數(shù)組長度賦值為0,同樣可以清空數(shù)組,并且同時能實現(xiàn)數(shù)組重用,減少內存垃圾的產(chǎn)生。

總的來說,就是盡量避免使用比如pop、slice等產(chǎn)生多余變量的方法,除非本身是需要產(chǎn)生的結果的,然后復用現(xiàn)有的一些對象。

總結

現(xiàn)在再回到之前遺留的三個問題,相信大家心里都有答案了~目前雖然說大多場景存在性能過剩的情況,即使不關注這些也僅僅是幾十毫秒甚至微秒級別的差異,但是在一些特殊的場景下,要求還是比較高的,另外,作為前端開發(fā)工程師,即使工作中大多業(yè)務場景不需要我們關注這些,但是我覺得對其有一定的了解還是很有必要的。

好了,今天的分享就到這里,如果你是正在學習前端或準備學習前端,可以去我的前端學習交流裙(109029339)免費下載一些前端學習視頻,而且不定時還有大咖直播分享,希望能幫助大家共同成長。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容