内存泄露是什么? 垃圾回收又是啥?
在使用C/C++这类语言的时候, 当我们想动态声明一个数组时, 一般需要手动分配内存, 之后还需要手动释放内存. 如果我们没有在不需要的时候释放内存的话, 就会产生内存泄露了.
我个人理解是, 内存泄露是内存里有一份你不会再用到并且你希望回收/销毁的数据.
在C/C++中内存泄露可能是比较常见的, 毕竟需要手动管理内存; 现在很多语言都有类似垃圾回收的机制, 或者是帮助内存管理的机制. 比如Rust的所有权, 离开作用域销毁. 其实我感觉所有权还挺好的, 比起垃圾回收来说. 至少知道什么东西还会在内存里, 什么东西不会在内存里并且知道什么时候会销毁.
javascript的垃圾回收
javascript依靠的是垃圾回收, 当我们声明一份数据时会自动分配内存, 理想情况下当我们不可能再使用这份数据时就会进行垃圾回收, 至于什么时候回收就不知道了.
引用计数法
现在已经不用这个方法了, 大概跟硬链接一样, 有一个计数. 每被引用一次就+1, 引用销毁了就-1, 比如
let obj = {a: 1, b: 2} // 声明了一个对象{a: 1, b: 2}, 然后obj引用了这个对象, 引用+1
let obj1 = obj // obj1也引用了这个对象, 引用+1
let a = obj.a // 又有一个东西引用了, +1
obj = null // obj不再引用了, -1
obj1 = null // 再-1
a = 0 // -1
// 引用计数为0了, 假设代码在这里就结束了, 一般情况下可以回收分配给{a: 1, b: 2}的内存了
引用计数法有个问题, 就是循环引用.
function a() {
let obj1 = {}
let obj2 = {}
obj1.www = obj2
obj2.www = obj1
}
a()
这样即使离开了作用域, 由于彼此引用着对方, 这两块内存都不能被回收, 造成了内存泄露.
标记清除法
标记清除法还可以细分, 不过我不太清楚了, mdn好像也没讲.
大概就是引擎会回收看上去无法触及的数据, 比如
function a() {
let obj1 = {}
let obj2 = {}
obj1.www = obj2
obj2.www = obj1
}
a()
a执行完后, obj1和obj2不可能在外面访问了, 因此可以安全地回收内存.
<script>
function a() {
let obj1 = {}
let obj2 = {}
obj1.www = obj2
obj2.www = obj1
return obj1
}
const obj = a()
</script>
这样的话, 浏览器是不会回收里面的obj1和obj2的, 因为可以通过obj来访问它们.
闭包与内存泄露
这两个东西其实没什么太大关联, 闭包也不一定导致内存泄露, 只是可能会出现不小心犯下的错误.
闭包=函数+词法环境. 这个词法环境我个人理解就是上下文.
当我们创建一个函数时, 其实就创建了一个闭包了. 只是这个词法环境是全局的而已.
function a() {
const www = {};
function doSomething() {
www.a = 1
}
return doSomething
}
let doSomething = a()
这样算是创建了一个常见的闭包函数了. 同样的, 里面www对应的{}不会被回收, 因为可以通过doSomething来访问. 当我们需要回收www对应的{}时, 只需要手动将doSomething设置为null就行了.
闭包的特殊情况
闭包存在一种特殊情况, 即使不可达也不能回收.
function a() {
const www = {}
function something() {
console.log(www)
}
function elseThing() {
console.log(1)
}
return elseThing
}
const b = a()
这里很明显www是不可达的, 但是因为www在elseThing的语法环境里, 只要b可达, www就不会销毁.
可以简单试验一下:
<button id="button">点我!</button>
<script>
function exampleFunction() {
const elements = new Array(1000000).fill(0).map((_, i) => {
const element = document.createElement('div');
element.textContent = `Element ${i}`;
return element;
})
function something() {
console.log(elements)
}
function elseThing() {
console.log('else')
}
return elseThing
}
let example = null
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('button');
button.addEventListener('click', () => {
example = exampleFunction();
})
});
</script>
在index.html写下类似这样的东西, 很明显执行这个exampleFunction后我们不可能访问得到elements.
在点击按钮之前, 在开发者工具的内存选项卡中, 先清一下内存, 然后拍摄堆快照. 第一个按钮就是了.
然后再点按钮, 拍一个堆快照, 清理内存后再拍一个.
会发现第二次和第三次的堆快照几乎一样, 并且远大于第一次的.
没意外的话在旁边可以看到Array排第一个, 然后里面第一个数组都是div.
内存管理果然还是Rust好啊.