第一百一十篇:記憶體洩漏和垃圾回收(JS)
好傢伙,本篇內容為《JS高階程式設計》第四章的學習筆記
1.記憶體洩露
1.1.什麼是記憶體洩漏?
記憶體洩漏(Memory Leak)是指程式中已動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。 記憶體洩漏缺陷具有隱蔽性、積累性的特徵,比其他記憶體非法訪問錯誤更難檢測。因為記憶體洩漏的產生原因是記憶體塊未被釋放,屬於遺漏型缺陷而不是過錯型缺陷。 ————百度百科1.2.記憶體洩漏會導致什麼後果?
1.3.洩露原因
在C語言中,從變數存在的時間生命週期角度上,把變數分為靜態儲存變數和動態儲存變數兩類。 靜態儲存變數是指在程式執行期間分配了固定儲存空間的變數,而動態儲存變數是指在程式執行期間根據實際需要進行動態地分配儲存空間的變數。 在記憶體中供使用者使用的記憶體空間分為三部分:- 程式儲存區
- 靜態儲存區
- 動態儲存區
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的方式來解除一個值的引用,從而釋放其記憶體空間