垃圾回收(Garbage Collection)是一种自动的内存管理机制,当我们不再需要这个东西的时候,它会定期地被浏览器回收,不再占用内存。
以下内容主要参考:javascript.info/garbage-col…
一、判断什么是垃圾
垃圾:不再需要的一个东西,需要从内存中释放。
基本规则
1.全局变量都不是垃圾,都不能回收,任何时刻都有可能再次用到它。
var a=1
2.当前正在执行的函数,其内部的局部变量和参数都不是垃圾
所有变量都有生命周期,只有函数fn执行的时候,b才产生在内存里,局部变量在函数执行完退出的时候就变成了垃圾,没有可能再用到它,下次再调用函数的时候,产生的是新的局部变量b。
var a=1 //全局变量,不是垃圾
function fn(){
var b=2
console.log(b)
}
fn() //第一次调用fn 内存中出现局部变量b 函数执行完成之后,b就变成了垃圾被回收
console.log(a)
fn() //第二次调用fn 产生新的局部变量b,不再沿用之前的b
3.永远不会变成垃圾的:
- window:永远都会可能用到,上面挂载了很多东西。window.Object/Array/Promise
- 全局作用域的变量:上例中的全局变量a、全局函数fn
引用 Reference
单引用
如果全局变量中有一个对象a,该对象a被认为是可访问的,并且对象a引用另一个对象的属性b,那么b也是可访问的,它们都不会被当成垃圾回收,详细示例如下:
let user = {
name: "John"
};
全局变量"user"引用该对象{name: "John"},二者都是可访问的。
如果user的值被置为null,则引用丢失,{name: "John"}成为垃圾被回收:
user = null;
双引用
将引用从user复制一份到admin:
let user = {
name: "John"
};
let admin = user;
user = null;
将user值置为null,{name: "John"}仍然可以通过admin全局变量访问,所以它仍在内存中。如果我们也置空了admin,那么{name: "John"}就可以被当成垃圾回收,从内存中删除。
相互关联引用
这是一段比较复杂的引用:
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({ name: "John"}, { name: "Ann" }); //一个新对象
下图中的所有对象都是可访问的:
删除两个引用后:
delete family.father;
delete family.mother.husband;
一个对象没有被指向的引用,仅有指向别人的引用是不能被访问的,会成为垃圾被回收。因此对象{ name: "John"}不能够被访问,成为垃圾:
垃圾回收后,真实留在内存中的:
闭环引用
虽然每个对象都有被其他对象引用,但是整个相互关联的对象形成闭环,仍然是外界无法访问的,会成为垃圾被回收。将上例的初始状态修改为:
family = null;
内存将会变成:
二、浏览器如何进行垃圾回收
有两种基本的垃圾收集算法,分别采用了遍历和计数的思想。
标记-清除算法(Mark-and-Sweep)
浏览器从全局作用域(根)开始,找全局变量,去找它们的引用,一层层的向下标记,如果这些变量引用了其他的变量就接着再向下标记,一直遍历找到到所有可访问的对象,最后,将未标记的对象清除掉。
- 优点:简单
- 缺点:速度慢。整体遍历,再标记。如果对象太多,标记会很慢,如果后面代码中把某个引用删掉,每次都需要重新标记。
改进:
- 分代收集。对象分成新代(临时的函数调用,马上标记马上删除)和老代(window对象),检查新老对象的频率不同,如果一个对象停留的时间越久,那么后面用到它的时间可能也越久,就隔很久再去看一次。
- 增量收集。一次只检查1000个节点,然后执行JS,过后再遍历1000个,再执行JS......,等把所有对象遍历完再垃圾回收。
- 空闲时间收集。等JS不执行的时候收集
引用计数算法(Reference Counting)
记录每个对象被引用的次数,全局变量都初始记为1,每次这个对象被创建引用就+1,删除引用了就-1,引用为0时就可以垃圾回收。
- 优点:可即刻回收垃圾,时间短,速度快,不用多次遍历。
- 缺点:计数任务繁重。