漫谈内存泄漏问题

发布于 6 年前
2 分钟阅读

在实现 Stack 数据结构的时候使用到了 WeakMap 来实现私有化。

使用 Map 其实也能实现私有化,但是 Map 存在内存泄露的风险。

WeakMap 与 Map 唯一的区别就是弱引用。


弱引用

WeakMap 的键只能以引用类型作为键,并且这个键是弱引用的。

弱引用的对象不参与引用计数。当 GC 发生时,若对象不存在强引用,该对象便会被回收。


Map 内存分析

let Stack = (function () {
    let map = new Map()
    class Stack {
        constructor() {
            this.dep = new Array(5 * 1024 * 1024)
            map.set(this, [])
        }
    }
    return Stack
}())

function logMemoryUsed() {
    console.log(`${process.memoryUsage().heapUsed / 1024 / 1024 >> 0} MB`)
}

global.gc()
logMemoryUsed()

let stack = new Stack()
global.gc()
logMemoryUsed()

stack = null
global.gc()
logMemoryUsed()

使用 node --expose-gc index.js ,输出

3 MB
43 MB
43 MB

Map 中的键为强引用,当 stack 置空后,map 仍持有 stack 对应的对象,使得内存空间不能被 GC 回收。而 map 是立即执行函数中的局部变量,外部访问不到,故使用 Map 实现私有化存在内存泄漏的风险。


关于数组占用空间说明

JS 中使用的是双精度浮点数,占用 8 个字节 64 位。数组默认分配一个元素的内存空间是 8 个字节。所以 5 M 长度的数组,占用空间为 40 M。


WeakMap 内存分析

let Stack = (function () {
    let weakMap = new WeakMap()
    class Stack {
        constructor() {
            this.dep = new Array(5 * 1024 * 1024)
            weakMap.set(this, [])
        }
    }
    return Stack
}())

function logMemoryUsed() {
    console.log(`${process.memoryUsage().heapUsed / 1024 / 1024 >> 0} MB`)
}

global.gc()
logMemoryUsed()

let stack = new Stack()
global.gc()
logMemoryUsed()

stack = null
global.gc()
logMemoryUsed()

使用 node --expose-gc index.js 运行该程序,输出

3 MB
43 MB
3 MB

weakMap 中的键为弱引用,当键的强引用计数为零的时候,对象会被 GC 回收。


引用计数法和标记清除算法

let Stack = (function () {
    let map = new Map()
    class Stack {
        constructor() {
            this.dep = new Array(5 * 1024 * 1024)
            map.set(this, [])
        }
    }
    return Stack
}())

function logMemoryUsed() {
    console.log(`${process.memoryUsage().heapUsed / 1024 / 1024 >> 0} MB`)
}

global.gc()
logMemoryUsed()

let stack = new Stack()
global.gc()
stack = null
global.gc()

setInterval(() => {
    global.gc()
    logMemoryUsed()
}, 5000)

输出

3 MB
43 MB
43 MB
43 MB
43 MB
43 MB
43 MB
43 MB
43 MB
43 MB
43 MB
43 MB
3 MB
3 MB

由上可知,内存在大约 1 分钟后将 map 和实例对象回收了。 map 属于实例对象的成员变量,而 map 中引用了实例对象,产生了循环引用。按照引用计数法两个对象都不能被 GC 回收。 引用计数法是最初级的垃圾回收机制。从 2012 年开始,所有的现代浏览器都是使用了标记清除算法。 标记清除算法会从全局对象开始查询,无法查询到的对象都将被清除,那么那些循环引用的对象也将被清除。


参考

ECMAScript 6 入门——阮一峰 内存管理——MDN

  • 弱引用
  • Map 内存分析
    • 关于数组占用空间说明
  • WeakMap 内存分析
  • 引用计数法和标记清除算法
  • 参考