WeakMap 和 Map 的区别,WeakMap 原理

4 阅读7分钟

WeakMap 和 Map 的区别,WeakMap 原理

目录

[TOC]

垃圾回收机制

我们知道,程序运行中会有一些垃圾数据不再使用,需要及时释放出去,如果我们没有及时释放,这就是内存泄露

JS 中的垃圾数据都是由垃圾回收(Garbage Collection,缩写为 GC)器自动回收的,不需要手动释放,它是如何做的喃?

很简单,JS 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,观察对象是否可被访问,然后按照固定的时间间隔周期性的删除掉那些不可访问的对象即可

现在各大浏览器通常用采用的垃圾回收有两种方法:

  • 引用计数
  • 标记清除
引用计数

最早最简单的垃圾回收机制,就是给一个占用物理空间的对象附加一个引用计数器,当有其它对象引用这个对象时,这个对象的引用计数加一,反之解除时就减一,当该对象引用计数为 0 时就会被回收。

该方式很简单,但会引起内存泄漏:

// 循环引用的问题
function temp(){
    var a={};
    var b={};
    a.o = b;
    b.o = a;
}

这种情况下每次调用 temp 函数, ab 的引用计数都是 2 ,会使这部分内存永远不会被释放,即内存泄漏。现在已经很少使用了,只有低版本的 IE 使用这种方式。

标记清除

V8 中主垃圾回收器就采用标记清除法进行垃圾回收。主要流程如下:

  • 标记:遍历调用栈,看老生代区域堆中的对象是否被引用,被引用的对象标记为活动对象,没有被引用的对象(待清理)标记为垃圾数据。
  • 垃圾清理:将所有垃圾数据清理掉

在我们的开发过程中,如果我们想要让垃圾回收器回收某一对象,就将对象的引用直接设置为 null

var a = {}; // {} 可访问,a 是其引用
 
a = null; // 引用设置为 null
// {} 将会被从内存里清理出去

但如果一个对象被多次引用时,例如作为另一对象的键、值或子元素时,将该对象引用设置为 null 时,该对象是不会被回收的,依然存在

var a = { b: 1 }; // 创建对象,a 引用它
var arr = [a];    // arr[0] 也引用它
a = null;         // a 不再引用,但 arr[0] 还在引用!
 
console.log(arr)
// [{}]

在 JavaScript 中, 垃圾回收(GC)机制是基于“引用计数”或“可达性(reachability)”的

  • 你创建了一个对象 { b: 1 } ,并把它赋值给变量 a

  • 然后你又把这个对象放进数组 arr 中,所以这个对象现在有两个引用:

    1. 一个是 a 变量的引用;

    2. 一个是 arr[0] 的引用。

当你执行 a = null; 时, 只是取消了 a 对这个对象的引用 ,但这个对象仍然被 arr 引用着。

因此, 对象 { b: 1 } 并没有变成“不可达”,垃圾回收器不会回收它


如果作为 Map 的键喃?

var a = {}; 
var map = new Map();
map.set(a, '三分钟学前端')
 
a = null; 
console.log(map.keys()) // MapIterator {{}}
console.log(map.values()) // MapIterator {"三分钟学前端"}

如果想让 a 置为 null 时,该对象被回收,该怎么做喃? 继续往下看

nodejs中的内容与gc
1. process.memoryUsage

这是 Node.js 提供的一个方法,用来查看当前 Node.js 进程使用的内存情况。

语法:

const memoryUsage = process.memoryUsage();
console.log(memoryUsage);

返回值是一个对象,包含以下字段(单位为字节):

字段说明
rss常驻内存集,表示进程占用的所有内存,包括代码、栈、堆等。
heapTotalV8 分配的堆内存总量。
heapUsed实际使用的堆内存。
externalC++ 对象绑定到 JavaScript 的内存,比如 Buffer 占用的内存。
arrayBuffers用于 ArrayBuffer 和相关视图的内存。

示例输出:

{
  rss: 23437312,
  heapTotal: 8257536,
  heapUsed: 4129600,
  external: 8272,
  arrayBuffers: 9386
}

2. node --expose-gc

这是启动 Node.js 进程时的一个参数,用于 显式暴露垃圾回收函数 global.gc()

Node.js 默认是不会让你手动调用垃圾回收(Garbage Collection)的,但是加上这个参数后,就可以使用 global.gc() 来手动触发一次垃圾回收。

用法:

node --expose-gc yourfile.js

然后你在代码里就可以写:

global.gc();

3. global.gc()

这是在使用 --expose-gc 参数时,Node.js 提供的手动触发 V8 垃圾回收的方法。

注意: 这个方法不会在默认情况下存在,必须通过 --expose-gc 启动。

使用场景:

  • 做内存占用测试时用来在关键点手动清理内存。

  • 不建议在生产环境中频繁使用,会带来性能损耗。


示例整合
// 启动时请加:node --expose-gc test.js
 
console.log('Before GC:', process.memoryUsage());
global.gc();  // 手动触发 GC
console.log('After GC:', process.memoryUsage());

WeakMap vs Map

ES6 考虑到了这一点,推出了: WeakMap 。它对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)。

Map 相对于 WeakMap

  • Map 的键可以是任意类型, WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键
  • Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键; WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的
  • Map 可以被遍历, WeakMap 不能被遍历

下面以 WeakMap 为例,看看它是怎么上面问题的:

var a = {}; 
var map = new WeakMap();
map.set(a, '三分钟学前端')
map.get(a)
 
a = null; 

上例并不能看出什么?我们通过 process.memoryUsage 测试一下:

//map.js
global.gc(); // 0 每次查询内存都先执行gc()再memoryUsage(),是为了确保垃圾回收,保证获取的内存使用状态准确
 
function usedSize() {
    const used = process.memoryUsage().heapUsed;
    return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
 
console.log(usedSize()); // 1 初始状态,执行gc()和memoryUsage()以后,heapUsed 值为 1.64M
 
var map = new Map();
var b = new Array(5 * 1024 * 1024);
 
map.set(b, 1);
 
global.gc();
console.log(usedSize()); // 2 在 Map 中加入元素b,为一个 5*1024*1024 的数组后,heapUsed为41.82M左右
 
b = null;
global.gc();
 
console.log(usedSize()); // 3 将b置为空以后,heapUsed 仍为41.82M,说明Map中的那个长度为5*1024*1024的数组依然存在

执行 node --expose-gc map.js 命令:

其中, --expose-gc 参数表示允许手动执行垃圾回收机制

// weakmap.js
function usedSize() {
    const used = process.memoryUsage().heapUsed;
    return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
 
global.gc(); // 0 每次查询内存都先执行gc()再memoryUsage(),是为了确保垃圾回收,保证获取的内存使用状态准确
console.log(usedSize()); // 1 初始状态,执行gc()和 memoryUsage()以后,heapUsed 值为 1.64M
var map = new WeakMap();
var b = new Array(5 * 1024 * 1024);
 
map.set(b, 1);
 
global.gc();
console.log(usedSize()); // 2 在 Map 中加入元素b,为一个 5*1024*1024 的数组后,heapUsed为41.82M左右
 
b = null;
global.gc();
 
console.log(usedSize()); // 3 将b置为空以后,heapUsed 变成了1.82M左右,说明WeakMap中的那个长度为5*1024*1024的数组被销毁了

执行 node --expose-gc weakmap.js 命令:

上面代码中,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。由此可见,有了它的帮助,解决内存泄漏就会简单很多。

最后看一下 WeakMap

WeakMap

WeakMap 对象是一组键值对的集合,其中的 键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

属性:

  • constructor:构造函数

方法:

  • has(key):判断是否有 key 关联对象
  • get(key):返回key关联对象(没有则则返回 undefined)
  • set(key):设置一组key关联对象
  • delete(key):移除 key 的关联对象
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
 
myWeakmap.set(myElement, {timesClicked: 0});
 
myElement.addEventListener('click', function() {
  let logoData = myWeakmap.get(myElement);
  logoData.timesClicked++;
}, false);