1.对于js来说什么是垃圾?
垃圾就是没用了的东西.emmm~~对于js来说,这种说法不是特别准确但是又特别贴切.占着内存但是又不被需要的变量被称为垃圾(有被内涵到).
js的内存管理中有个很关键的概念,叫"可达性".变量直接的引用关系是树状的,在某个作用域中,其全局变量作为根,往下找引用关系基本就能理清所有的变量之间的关系,某个变量能通过这种方式找到,它就是"可达"的.
比如:
var a =[{name:'小明',age:18}] let b = a[0] let c = b?.age console.log(c)
由此可见,从根a开始可以一步步抵达c,也就是说c和b都是可达的,它们不是垃圾.还有个固定规则就是,全局变量默认可达.全局变量这个"根"都不可达了,那就没有可达的了.
以下情况是比较常见的垃圾
(1)使用完并且不被依赖的函数变量.
function f1(){ const a ={name:'小明'} console.log(1) } f1()
因为这个a是个函数变量,并且只在这个函数中被需要,函数执行完.这个变量自然就成了垃圾被回收了.有函数的情况下,垃圾回收机制是包含栈垃圾回收和堆垃圾回收的.
(2)变量不再需要堆
let a = {name:'小明'} a = null
这个跟(1)情况不同,这个是a这个变量还在,但是它由引用数据类型变成了null,也就是说原来的堆内存不需要了,就被回收了."{name:'小明'}"这个值原来所占的内存就空出来了.我们再举一个例子
let a ={ name:'小明'} let b = a a =null console.log(a,b)
这种情况跟上一个情况不一样了,虽然a不需要堆内存了,但由于b和a共享了一个内存,b还需要堆去存放"{name:'小明'}"这个值.所以这个堆的内存没有回收.
(3)"根"被切断的变量(......啊这)
function fn(obj){ return { son:obj } } let me =fn({name:'小明'}) me = null
这个例子里面,"me"是全局变量,是"根",而"{name:'小明'}"这个对象刚开始是被me.son所需要的,所以他被保留在内存里.但是,me = null 也就是说 "me"不需要"son"属性了,自然不需要"{name:'小明'}"这个对象了.也就是说它的根被切断了,它就是不可达的了.它就变成了垃圾被回收了.
我们在这里讨论的时候主要还是讨论,什么时候回收"堆"里的内存.所以主要观察对象应该是"内存"而不是变量.
2.怎么回收?
(1)计数法:简单来说就是判断每个变量被需要的次数,每多一次就+1,每次实际被引用就-1,其次数变成0了就回收.
这个方法现在基本不用了,因为无法解决循环引用的问题.何为循环引用?
function fn(){ let a ={} let b ={} a.name =b b.name =a return } fn()
因为互相需要,但是并没有真正被使用,所以其计数器永远不为0.但是,我提出来这个方法的重点是在于,这个现象和我之前说的"因为这个a是个函数变量,并且只在这个函数中被需要,函数执行完.这个变量自然就成了垃圾被回收了"冲突.这是为什么?
由此可见:垃圾回收机制和作用域是两个独立的概念,即无论在哪个作用域里,其变量和内存的回收都要遵循垃圾回收机制.
然后我们在回头理一下,我们常说,函数里的变量,函数使用完就被回收了,这是常规情况,也就是说大部分情况是符合垃圾回收机制的.但也有特殊,比如上面说的循环调用的情况(仅限垃圾回收机制是计数法的时候),还有一种比较常规的特殊情况---闭包.
同样是函数里申明的变量,但是它被外包的变量所需要,它的内存就暂时不能释放,于是就是当前函数被执行了,其中的变量的内存仍然未被回收.具体可以看我的另一篇博客:闭包 .这有利于我们从垃圾回收机制的角度上去理解闭包.
(2)标记清除
这是目前浏览器主要使用的方法.其主要思路就是用了查找内存(变量)"可达性"的,并加以标记,"不可达"的内存清除回收.所以这种方法分为两步,标记和清除.
标记:逐步标记内存的可达性
清除:清除"不可达"的内存,并且将原来的标记给清除.
相比较而言,这种方案基本能覆盖所有情况,但它也有缺点.1是不能随时清除.2是清除出来的内存是碎片化的.
3.何时回收?
针对于2里的两个缺点,V8引擎做了一定的优化.首先就是回收时机,回收算法执行的时候是会中断js的主线程的,导致整个js的主线程停顿.所以,其采用了增量标记法.也就是说不一次性标记完,而且先标记,然后执行js然后再增量标记,然后再执行js以此类推,一直到标记完,再去清除和整理.大概用两张图说明一下
所以,它仍然不是随时都在清除的,只在空闲或者内存快占满的时候才执行回收机制.
4.标记整理法
我们上面说过标记法可能会导致内存碎片化,因为堆中的内存空间虽然是连续的,但是"垃圾"的排序并不是,也就意味着清除了垃圾后,空出来的空间是碎片化的.碎片化的内存可利用率并不高,所以,我们不光要清除,还得整理一下空间.于是在原来的标记删除的基础上再加上了,整理部分.V8采取的方案是,将内存分成两个部分,一部分是现在正在用的内存,另一部分空着,然后在整理的时候去把正在用的内存里的数据一个个复制到空的那部分内存里,因为是自己挪过来,我就可以按顺序排列好了,然后再把原来空间删除.最终,这两部分实现了数据交换,而且原来乱序的数据也被排好了放在新的空间里,而原来那部分变成的空的.下次整理的时候又可以换回去了
总结:不被需要的内存会被作为垃圾回收,js会在空闲时或者内存快满的时候去查找哪些内存是"不可达的",然后为了能更好的利用内存,特意空出一半内存去整理删除之后剩下的内存空间,保证内存使用率.