垃圾回收机制
GC是什么?
GC
即Garbage Collection
,程序工作过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前使用过,以后不在会使用的内存空间,而GC
负责回收这些垃圾的。
JS中的垃圾回收机制
JS垃圾回收机制
- JS具有自动回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JS有全局变量和局部变量。 全局变量会一直保存在内存中,直到页面卸载才回收变量内存;局部变量声明在函数内部,会在函数执行结束后回收内存。
- 当使用闭包时,函数内部定义的局部变量会一直留在内存中,不会被使用。 所以尽量避免使用闭包,以免造成内存泄漏。
垃圾回收方式
标记清除
- 当变量进行执行环境时,就标记这个变量
进入环境
,被标记为进入环境
的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为离开环境
,被标记为离开环境
的变量会被内存释放。 - 垃圾收集器在运行时会给存储在内存中的所有变量加上标记。然后,它会去掉环境中的变量和被环境中的变量引用的标记,剩下的变量将被视为需要删除的变量,垃圾收集器完成内存清除工作,销毁那些带有标记的值并回收他们所占用的内存。
标记清除法的优点:
实现简单
标记清除法的缺点:
在清除之后,剩余的对象内存位置是不变的,会导致内存空间不是连续的,出现了内存碎片。并且由于剩余空间内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配问题。
假设我们需要为新建的对象分配大小为N的空间,由于当前空闲的内存是间断的,不连续的,则需要对空闲内存列表进行一次单向的遍历,找出内存空间大于等于N的块,才能为其分配内存空间。
标记清除算法
有两个明显的缺点:
- 内存碎片化。空间内存块是不连续的,容易出现很多空闲内存块,还可能出现分配所需内存过大的对象时找不到合适的块。
- 分配速度慢。每次分配内存都是一个O(n)的操作,分配效率慢。
引用计数
- 引用计数就是跟踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1;
- 如果同一个值又被赋值给另一个变量,那该值的引用次数加1;
- 相反,如果包含这个值引用的变量又取得另一个值,则这个值的引用次数就减1;
- 当这个引用次数变为0,说明这个变量就没有用了,在下次垃圾回收器下次运行时会回收内存。
引用计数会引起一个问题:循环引用。例如:
function fun(){
let obj1 = {}
let obj2 = {}
obj1.a = obj2 // obj1引用obj2
obj2.a = obj1 // obj2引用obj1
}
在上面的例子中,obj1和obj2相互引用,两个对象的引用次数都为2.当函数执行完后,两个对象离开作用域,但obj1和obj2的引用次数还是2,不会减为0,这样就不会被回收。
解决方式就是:手动释放内存。
obj1.a = null
obj2.a = null
减少垃圾回收
虽然浏览器可以进行自动垃圾回收,但当代码比较复杂时,垃圾回收的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化:在清空一个数组时,将其赋值为
[]
- 对object进行优化:对象尽量复用,对于不再使用的对象,赋值为
null
- 对函数进行优化:在循环中的
函数表达式
,如果可以复用,尽量放在函数外部
浏览器 V8引擎的垃圾回收机制
V8的垃圾回收机制是怎样的
V8采用了分布式垃圾回收机制,将内存分为新生代和老生代两个部分。
新生代算法
新生代中的对象一般存活时间较短,使用Scavenge GC算法。Csavenge GC算法具体实现中,主要采用了一种复制式的方法,即Cheneny算法
。
在新生代空间中,Cheney算法
将内存空间分为两部分,别分为From空间和To空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From空间中,当From空间被占满时,新生代GC就会启动了。算法会检查From空间中存活的对象并复制到To空间中,如果有失活的对象就会销毁。当复制完成后From空间和To空间互换,GC结束。
有两种情况会使新生代中的对象移到老生代中:
- 当一个对象经过2次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理。
- 如果复制一个对象到空闲区时,空闲区空间占用超过了25%,那么这个对象会被直接晋升到老生代空间中。设置25%的原因是当完成Scanvenge回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将影响后续的内存分配。
老生代算法
老生代中的对象一般存活时间较长且占用空间大,因为老生代中的对象通常比较大,如果再用新生代赋值的方法就会非常耗时,从而导致回收执行效率不高。老生代使用了两个算法,分别是标记清除算法和标记压缩算法。
标记清除算法和浏览器回收机制的标记清除算法相同。
并行回收
全停顿:JS是单线程的,当进行垃圾回收时就会阻塞当前的JS脚本的执行,需等待垃圾回收完毕后再回复脚本执行,这种行为就是全停顿。如果某次GC时间过长,那么对用户来说就会造成页面卡顿的情况,所以有了并行回收。
而并行回收就是使用多个辅助线程,与主线程同时进行垃圾回收,加快回收速度。但是主线程还是要让出。
并发回收
并行回收依然可能阻塞主线程,而是用并发回收,辅助线程可以在后台完成执行垃圾回收的操作,主线程也可以自由执行不被挂起。
并发回收的缺点:在一边进行垃圾回收一边执行JS时,堆中的对象引用关系随时都会发生变化,辅助线程之前做的一些标记或者正在进行的标记就会发生改变,所以需要额外的锁来进行限制。
标记压缩算法
标记压缩算法可以有效解决标记清除法的缺点。在标记结束后,标记压缩算法会将活着的对象向内存的一端移动,最后清理掉边界的内存。
哪些行为会引起内存泄漏
- 意外的全局变量:在js中未对变量进行声明直接赋值的话,该变量会被当作全局变量。如果有大量的变量没有声明,会出现大量的全局变量,导致内存泄漏。
- 闭包:在使用闭包后,可以使我们访问到函数内部的变量和函数,当函数执行完毕后,这些变量会被保留在内存中,仍然可以使用,不会被回收。所以如果大量使用闭包,会导致内存泄漏。
- 引用了DOM元素,删除了DOM元素后,该引用还被保留在内存中没被删除
- 设置了setInterval定时器但忘记取消它:设置了 setInterval 定时器,而忘记取消它,如果在定时器中有循环函数有对外部变量的引用的话,那么这个变量会被一直保留在内存中,无法被回收。