通過【垃圾回收機制】的角度認識【Map與WeakMap】的區(qū)別

原文鏈接: https://www.zhihu.com/people/yiqun-a-qia
前言:前段時間剛接觸WeakMap時概念很模糊,查了很多文章大部分是簡單掠過,后來通過學習了垃圾回收機制后有感而發(fā)。
簡單介紹JavaScript中的垃圾回收
JavaScript是使用垃圾回收的語言,也就是說執(zhí)行環(huán)境負責在代碼執(zhí)行時管理內存。
眾所周知,我們一般使用 javascript 進行開始都很少關注垃圾回收是如何進行的,
對于開發(fā)者來說,JavaScript 的內存管理是自動的、無形的。
于是乎對于學習WeakMap這個概念需要用到垃圾回收相關知識,
先簡單描述下垃圾回收是如何進行。
例子:“我有一個朋友”
沒有什么是用例子解決不了的
// 從前,我有一個朋友。
let myFriend = {
info: "I have a friend",
attr: "Lsp"
}

這里我們在全局空間聲明了一位“朋友” myFriend

他的引用地址指向了{ info: "I have a friend", attr: "Lsp" }
myFriend = null;
// 此時我們已經沒有這位朋友了

由于我們與這位“朋友”斷了聯(lián)系,
從 根(window/global) 開始 查詢 找不到 其引用,此時垃圾回收機制會把它當作垃圾進行自動回收,并釋放內存。
多個引用
現(xiàn)在我們再舉一個例子,聲明多個變量指向同一個引用
// 大家好,我是...
let me = null;
// 然后,我有一個朋友他...
let myFriend = {
info: "I have a friend",
attr: "Lsp"
}
// (....?)
me = myFriend;
你說的這個朋友是不是你.jpg

由此可見目前有兩個變量引用地址指向了同一塊地方 { info: "I have a friend", attr: "Lsp" }。
我們現(xiàn)在將其中一個變量進行斷開。
// 我說的那位朋友真的不是我!
me = null;

即使將其中一個變量的引用地址斷開
另外一個 "朋友"變量 任然繼續(xù)引用
(也就是說對象還是可以通過根查找到)
所以垃圾回收機制不會將它進行回收。
回收策略
JavaScript最常用垃圾回收策略是"標記清理(mark-and-sweep)"
策略的大意即為:
遍歷空間下所有的對象,并標記活著的,有被引用的并且最終可以到達根(window/global)的對象。
在垃圾回收階段的時候,將沒有標記進行清除。
這里回收策略不是本章重點,具體策略在底層代碼上還會再細分,以及內存分代對應具體算法,暫時不展開講,有興趣可以查閱樸靈《深入淺出Node.js》以及V8垃圾回收機制相關文章。
JavaScript中Map與WeakMap
上面贅述那么多終于來講本文關鍵了,
提前講上文原因在于WeakMap 的特點與垃圾回收機制有關。
我們引用一下《JavaScript高級程序設計(第四版)》的原話。
ECMAScript6新增的”弱映射“(WeakMap)是一種新的集合類型,為這門語言帶來了增強的鍵值對存儲機制。WeakMap是Map的”兄弟“類型,其API也是Map的子集。WeakMap中的”weak“(弱),描述的是JavaScript垃圾回收程序對待的”弱映射“中鍵的方式。---- 《JavaScript高級程序設計(第四版)》6.5
嗯,說得好棒很詳細的樣子!
可是我完全不懂呢( 嗯嗯嗯我完全理解了呢.jpg
Map與WeakMap簡單區(qū)別
Map的鍵值可以是原始數(shù)據(jù)類型和引用類型,WeakMap的鍵值只能說引用類型(object)
Map可以迭代遍歷鍵,WeakMap不可迭代遍歷鍵
WeakMap中的”weak“表示弱映射的鍵是”弱弱地拿著“的,意思就是,這些鍵不屬于正式的引用。
換言之,WeakMap所構建的實例中,
其key鍵所對應引用地址的引用斷開或不屬于指向同一個內存地址的時候,
其對應value值就會被加入垃圾回收隊伍。
(粗暴理解為:因為key必須是個引用類型,當key引用斷了或變了,這個鍵值對就可以進垃圾桶了)
觀察內存空間理解WeakMap
因為通常條件下很難察覺WeakMap里面keyValue什么時候消失
但是通過某一個引用類型的值大到足夠占據(jù)一定內存時候
我們可以通過觀察內存的變化來觀察WeakMap的特性
示例使用的Node.js的進程Apiprocess.memoryUsage()配合手動垃圾回收global.gc()在終端觀察,
也可以使用Chrome瀏覽器Performance功能錄制內存變化,
但是為了方便用代碼展示就依次展開了,0 .0 本質觀察到內存變化即可,手段可以有多種。
glabal.gc()
手動調用一次垃圾回收。需要在運行js文件時候增加命令 --expose-gc,一般環(huán)境下不推薦使用,這里做學習用。
process.memoryUsage()
查看Node進程的內存占用情況。
返回值為對象其中包含五個屬性 rss,heapTotal,heapUsed,external,arrayBuffers;
其中主要屬性是 heapTotal和heapUsed對應的是V8的堆內存信息。
heapTotal是堆中總共申請的內存量,heapUsed表示目前堆中使用的內存量。單位都為字節(jié)。
現(xiàn)在我們通過代碼來展示
// index.js
// 第一次手動清理垃圾以確保為最新狀態(tài),觀察內存情況
global.gc();
console.log(`第一次垃圾回收,當前內存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`);
const wm = new WeakMap();
let key = {};
// 給 WeakMap實例 賦值一個 占領內存足夠大的 鍵值對
wm.set(key, new Array(114514 * 19));
// 手動清理一下垃圾 觀察內存占用情況
global.gc();
console.log(`第二次垃圾回收,當前內存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`);
// 此時把 key鍵 的引用進行斷開,并觀察內存占用情況
key = null;
// key = new Array();
// 這種改變引用地址寫法也可以引起 弱映射,因為引用地址不再是同塊內存地址 WeakMap內對應的value也會被垃圾回收
global.gc();
console.log(`第三次垃圾回收,當前內存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`);
$ node --expose-gc index.js第一次垃圾回收,當前內存使用情況:1.66MB
第二次垃圾回收,當前內存使用情況:18.45MB
第三次垃圾回收,當前內存使用情況:1.84MB
那么我們來看看Map的情況,
與上方index.js的代碼一致,把new WeakMap()換成new Map()。
看看終端輸出效果
$ node --expose-gc index.js
第一次垃圾回收,當前內存使用情況:1.66MB
第二次垃圾回收,當前內存使用情況:18.45MB
第三次垃圾回收,當前內存使用情況:18.44MB很明顯我們將key = null的引用地址斷開后 ,
value 仍然存在Map所構建的實例里面,一如既往還在內存里面。
現(xiàn)在我們將代碼場景改成Map的樣子
// index.js
// 第一次手動清理垃圾以確保為最新狀態(tài),觀察內存情況
global.gc();
console.log(
`第一次垃圾回收,當前內存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`
);
const m = new Map();
let key = {};
m.set(key, new Array(114514 * 19));
// 手動清理一下垃圾 觀察內存占用情況
global.gc();
console.log(
`第二次垃圾回收,當前內存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB,
當前Map的長度: ${m.size}`
);
// 此時把 key鍵 的引用進行斷開,并觀察內存占用情況
key = null;
global.gc();
console.log(
`第三次垃圾回收,當前內存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB,
當前Map的長度: ${m.size}`
);
// 清除Map所有鍵值對
m.clear();
global.gc();
console.log(
`第四次垃圾回收,當前內存使用情況:${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB,
當前Map的長度: ${m.size}`
);
$ node --expose-gc index.js
第一次垃圾回收,當前內存使用情況:1.66MB
第二次垃圾回收,當前內存使用情況:18.45MB,當前Map的長度: 1
第三次垃圾回收,當前內存使用情況:18.45MB,當前Map的長度: 1
第四次垃圾回收,當前內存使用情況:1.85MB,當前Map的長度: 0
由此可見Map所構建的實例是需要手動清理,才能被垃圾回收清除,
而WeakMap只要外部的引用消失,所對應的鍵值對就會自動被垃圾回收清除。
總結
通過堆內存分析后重新認識Map和WeakMap,
由于一開始接觸這個API的時候有點陌生,
查閱很多網(wǎng)上很多文章后描述的樣子非常表層一筆帶過
因為弱引用反正他就會被自動回收就是了不會占用內存balabala,總之就是一兩句話帶過
這讓我很頭疼,下定決定翻了很多的書結合理解才搞懂WeakMap存在的意義。
前期是在在《現(xiàn)代JavaScript教程》受到啟發(fā),
通過翻查《深入淺出Node.js》找到了驗證方法,
最后看到新版《JavaScript高級程序設計(第四版)》對WeakMap描述也是簡單帶過,
于是下定決心來寫這一篇文章。
第一次產出文章,有表達描述不到位,或者代碼邏輯錯誤的地方歡迎指出。
我是阿卡,一名普通搬磚的前端工程師。
最后
歡迎加我微信(winty230),拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...


