1. 程式人生 > 其它 >第一百一十篇:記憶體洩漏和垃圾回收(JS)

第一百一十篇:記憶體洩漏和垃圾回收(JS)

好傢伙,本篇內容為《JS高階程式設計》第四章的學習筆記

1.記憶體洩露

1.1.什麼是記憶體洩漏?

記憶體洩漏(Memory Leak)是指程式中已動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。 記憶體洩漏缺陷具有隱蔽性、積累性的特徵,比其他記憶體非法訪問錯誤更難檢測。因為記憶體洩漏的產生原因是記憶體塊未被釋放,屬於遺漏型缺陷而不是過錯型缺陷。                                                                   ————百度百科   

1.2.記憶體洩漏會導致什麼後果?

此外,記憶體洩漏通常不會直接產生可觀察的錯誤症狀,而是逐漸積累,降低系統整體效能,極端的情況下可能使系統崩潰。   隨著計算機應用需求的日益增加,應用程式的設計與開發也相應的日趨複雜,開發人員在程式實現的過程中處理的變數也大量增加,如何有效進行記憶體分配和釋放,防止記憶體洩漏的問題變得越來越突出。 例如伺服器應用軟體,需要長時間的執行,不斷的處理由客戶端發來的請求,如果沒有有效的記憶體管理,每處理一次請求資訊就有一定的記憶體洩漏。 這樣不僅影響到伺服器的效能,還可能造x成整個系統的崩潰。因此,記憶體管理成為軟體設計開發人員在設計中考慮的主要方面 [1]  。                                                                                                                                         ————百度百科
 

1.3.洩露原因

在C語言中,從變數存在的時間生命週期角度上,把變數分為靜態儲存變數和動態儲存變數兩類。 靜態儲存變數是指在程式執行期間分配了固定儲存空間的變數,而動態儲存變數是指在程式執行期間根據實際需要進行動態地分配儲存空間的變數。 在記憶體中供使用者使用的記憶體空間分為三部分:
  • 程式儲存區
  • 靜態儲存區
  • 動態儲存區
程式中所用的資料分別存放在靜態儲存區和動態儲存區中。 靜態儲存區資料在程式的開始就分配好記憶體區,在整個程式執行過程中它們所佔的儲存單元是固定的,在程式結束時就釋放,因此靜態儲存區資料一般為全域性變數 動態儲存區資料則是在程式執行過程中根據需要動態分配和動態釋放的儲存單元
,動態儲存區資料有三類函式形參變數、區域性變數和函式呼叫時的現場保護與返回地址。
由於動態儲存變數可以根據函式呼叫的需要,動態地分配和釋放儲存空間,大大提高了記憶體的使用效率,使得動態儲存變數在程式中被廣泛使用 開發人員進行程式開發的過程使用動態儲存變數時,不可避免地面對記憶體管理的問題。程式中動態分配的儲存空間,在程式執行完畢後需要進行釋放。 沒有釋放動態分配的儲存空間而造成記憶體洩漏,是使用動態儲存變數的主要問題。 一般情況下,開發人員使用系統提供的記憶體管理基本函式,如malloc、realloc、calloc、free等,完成動態儲存變數儲存空間的分配和釋放。 但是,當開發程式中使用動態儲存變數較多和頻繁使用函式呼叫時,就會經常發生記憶體管理錯誤                                                                                 ————百度百科   所以我這麼理解,一個變數不被上下文需要了,如果他還佔著那個記憶體,這個記憶體就"被浪費"了,無法使用, 就像這個記憶體洩露了一樣,這個現象被稱為記憶體洩露. (佔著茅坑不拉屎)    

2.垃圾回收

怎麼說, 書接上文,我們要把這個看似無用"變數"清除,從而釋放一部分記憶體空間 這個過程我們將它稱為垃圾回收  

2.1.JS垃圾回收

JavaScript是使用垃圾回收的語言,也就是說執行環境負責在程式碼執行時管理記憶體。在C和C++等

語言中,跟蹤記憶體使用對開發者來說是個很大的負擔,也是很多問題的來源。JavaScript 為開發者卸下了這個負擔,通過自動記憶體管理實現記憶體分配和閒置資源回收。

基本思路很簡單:確定哪個變數不會再使用,然後釋放它佔用的記憶體。

這個過程是週期性的,即垃圾回收程式每隔一定時間(或者說在程式碼執行過程中某個預定的收集時間)就會自動執行。

垃圾回收過程是一個近似且不完美的方案,因為某塊記憶體是否還有用,屬於“不可判定的”問題,意味著靠演算法是解決不了的。

我們以函式中區域性變數的正常生命週期為例。函式中的區域性變數會在函式執行時存在。

此時,棧(或堆)記憶體會分配空間以儲存相應的值。函式在內部使用了變數。然後退出。

此時,就不再需要那個區域性變量了,它佔用的記憶體可以釋放,供後面使用。這種情況下顯然不再需要區域性變量了,但並不是所有時候都會這麼明顯。

垃圾回收程式必須跟蹤記錄哪個變數還會使用,以及哪個變數不會再使用,以便回收記憶體。如何標記未使用的變數也許有不同的實現方式。

不過,在瀏覽器的發展史上,用到過兩種主要的標記策略:標記清理和引用計數

2.1.1.標記清理

JavaScript 最常用的垃圾回收策略是標記清理。當變數進入上下文,比如在函式

內部宣告一個變數時,這個變數會被加上存在於上下文中的標記。

而在上下文中的變數,邏輯上講,永遠不應該釋放它們的記憶體,因為只要上下文中的程式碼在執行,就有可能用到它們。

當變數離開上下文時,也會被加上離開上下文的標記。

給變數加標記的方式有很多種。比如,當變數進入上下文時,反轉某一位;

或者可以維護“在上下文中”和“不在上下文中”兩個變數列表,可以把變數從一個列表轉移到另一個列表。標記過程的實現並不重要,關鍵是策略。

垃圾回收程式執行的時候,會標記記憶體中儲存的所有變數(記住,標記方法有很多種)。

然後,它會將所有在上下文中的變數,以及被在上下文中的變數引用的變數的標記去掉。

在此之後再被加上標記的變數就是待刪除的了,原因是任何在上下文中的變數都訪問不到它們了。

隨後垃圾回收程式做一次記憶體清理,銷燬帶標記的所有值並收回它們的記憶體。

我的理解:給所有變數上個標記,在後文中,如果變數被引用了就將他的標記去掉,隨後,清理所有帶標記的變數

2.1.2.引用計數

另一種沒那麼常用的垃圾回收策略是引用計數(reference counting)。

其思路是對每個值都記錄它被引用的次數。宣告變數並給它賦一個引用值時,這個值的引用數為1。

如果同一個值又被賦給另一個變數,那麼引用數加1。

類似地,如果儲存對該值引用的變數被其他值給覆蓋了,那麼引用數減1。

當一個值的引用數為0時,就說明沒辦法再訪問到這個值了,因此可以安全地收回其記憶體了。垃

圾回收程式下次執行的時候就會釋放引用數為0的值的記憶體。

3.記憶體管理

記憶體管理 在使用垃圾回收的程式設計環境中,開發者通常無須關心記憶體管理。

不過,JavaScript 執行在一個記憶體管理與垃圾回收都很特殊的環境。

分配給瀏覽器的記憶體通常比分配給桌面軟體的要少很多,分配給移動瀏覽器的就更少了。

這更多出於安全考慮而不是別的,就是為了避免執行大量JavaScript的網頁耗盡系統記憶體而導致作業系統崩潰。

這個記憶體限制不僅影響變數分配,也影響呼叫棧以及能夠同時在一個執行緒中執行的語句數量。 

將記憶體佔用量保持在一個較小的值可以讓頁面效能更好。

優化記憶體佔用的最佳手段就是保證在執行程式碼時只儲存必要的資料。

如果資料不再必要,那麼把它設定為null,從而釋放其引用。

這也可以叫作解除引用

這個建議最適合全域性變數和全域性物件的屬性。

區域性變數在超出作用域後會被自動解除引用,

function createPerson(name) {

    let localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
let globalPerson = createPerson("Nicholas");

//解除globalPerson對值的引用

globalPerson = null;

在上面的程式碼中,變數globalPerson儲存著 createPerson()函式呼叫返回的值。

在createperson() 內部,localPerson建立了一個物件並給它添加了一個name屬性。

然後,localPerson作為函式值被返回,並被賦值給globalPerson。

localPerson在createPerson()執行完成超出上下文後會自 動被解除引用,不需要顯式處理。

但globalPerson 是一個全域性變數,應該在不再需要時手動解除其引用,最後一行就是這麼做的。

不過要注意,解除對一個值的引用並不會自動導致相關記憶體被回收。

解除引用的關鍵在於確保相關的值已經不在上下文裡了,因此它在下次垃圾回收時會被回收。

  小結:我們可以使用主動賦值null的方式來解除一個值的引用,從而釋放其記憶體空間