JavaScript GC简书

风流意气都作罢 提交于 2020-03-11 18:47:10

1.垃圾回收算法

垃圾:无法再被访问的对象或内存空间
延迟:指平均每次垃圾回收开始到结束需要的时间。
吞吐量:指平均一定时间内能回收多少内存,内存多少这个概念非常广泛,可以指多少个对象,也可以指多少字节的空间,具体的应该看指标应需求而异。
根节点:如全局变量上的对对象的引用、栈上对对象的引用等用户一定能够访问到的地址,是寻找活对象的入口。

下面简单地介绍引用计数、Mark-Sweep、Mark-Copy、Mark-Compact四种垃圾回收算法

1.1 引用计数

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。我们可以为每个对象都增加一个计数器,来记录对这个对象的引用数量,当引用计数归零时,这个对象变成了垃圾

引用计数的优点如下:

(1)内存释放及时,当一个对象死亡时其占用的内存马上被释放
(2)延迟低,内存释放的时间均匀地分布在各个时间段

缺点如下:

(1)每个对象需要附带一个计数字段的空间
(2)引用复制和销毁时需要对改变计数字段,这可能涉及到相对昂贵的原子操作
(3)无法处理循环引用,比如两个对象互相引用对方的情况

进阶话题:当一个大对象的引用归零时,常常会导致一大批的对象引用归零,这种成批释放的情况非常常见,会导致垃圾回收的延迟上升以及可能占用大量栈空间去递归释放循环引用可以通过一些算法检测到,也可以在适当时刻使用其他垃圾回收算法来释放引用计数器的更新是存在冗余的,即一大部分的引用计数的更新是可以被消除的

1.2 标记清除 Mark-Sweep

除去引用计数,Mark-Sweep是另一个思考方向。它分为**标记和清除**两个阶段。当垃圾回收被触发时,运行时从有限的根节点(在Javascript里,根是全局对象)出发,对所有能够到达的对象进行标记(一般为深度优先搜索),然后再遍历整个堆,清除所有未被标记的对象。

一般认为其优点有:

(1)相比引用计数很难处理循环引用,Mark-Sweep算法总能找到所有无法被引用的对象
(2)由于垃圾总被一起批量回收,可能可以提高内存回收的吞吐
(3)这个算法实现起来简单

缺点如下:

(1)每个对象需要附带至少一个比特作为标记的空间
(2)由于Mark阶段需要在整个堆上随机遍历,对CPU缓存不友好
(3)算法的性能与堆的大小相关,当堆非常大,而单次回收对象数量有限时,性能被严重拖累
(4)垃圾回收的延迟较高,会使用户代码完全停止一段时间
(5)出现内存不连续的状态

进阶话题:增量标记,即通过对算法一定的修改,Mark阶段可以与用户程序交替执行直到标记阶段完成,以减少垃圾回收算法的延迟。

1.3 标记复制 Mark-Copy

Mark-Copy将堆内存一分为二,一个处于使用状态,一个处于闲置状态。当开始垃圾回收时,会检查使用状态的内存块,把存活的对象复制到闲置状态的内存块,完成复制后,两个内存空间交换角色。

相较于Mark-Sweep,其优点有:

(1)在回收垃圾的同时也整理内存,避免了内存碎片化的问题
(2)非侵入式的算法,不需要对象上的字段(理想是美好的,但现实往往不是)
(3)算法的执行时间仅与活对象的数量有关,不需要扫描整个堆
(4)分配对象时不需要寻找空闲空间,因为其总在当前使用的堆的末尾

缺点如下:

(1)回收时需要进行大量的内存拷贝
(2)内存利用率低,维护了两个堆,却只用了一半的空间

进阶话题:
通过分块的方式维护N个堆,以提高内存利用率
对活对象进行分代维护

1.4 标记整理 Mark-Compact

注意Mark-Copy算法需要维护一个额外的堆来作为拷贝活对象的容器。标记整理和标记清除的差别在于对象标记死亡后,在整理内存的过程中,将活着的对象往一端移动,移动完成后,直接清理边界外的内存。

可以说Mark-Compact是Mark-Copy和Mark-Sweep算法的一种整合,其优缺点也只是前两种算法各取部分。

标记清除,标记复制,标记整理特点

(1)标记清除只复制活着的对象,用空间换取时间,速度最快
(2)标记复制只清除死亡的对象
(3)标记整理是两者的整合,速度最慢


2.V8 内存管理和垃圾回收机制

不同的引擎有不同的GC实现方式。这里就介绍V8 内存管理和垃圾回收机制

新生代和老生代

V8 将内存分为两类:新生代内存空间和老生代内存空间,新生代内存空间主要用来存放存活时间较短的对象,老生代内存空间主要用来存放存活时间较长的对象。对于垃圾回收,新生代和老生代有各自不同的策略。

 

new_old_generation.jpg

新生代主要使用Scavenge垃圾回收算法进行管理,主要实现是Cheney算法,将内存平均分为两块,使用空间叫From,闲置空间叫To,新对象都先分配到From空间中,在空间快要占满时将存活对象复制到To空间中,然后清空From的内存空间,此时,调换From空间和To空间,继续进行内存分配,当满足那两个条件时对象会从新生代晋升到老生代。也就是上面提到的标记复制式的算法

老生代主要采用Mark-Sweep和Mark-Compact算法,一个是标记清除,一个是标记整理。两者不同的地方是,Mark-Sweep在垃圾回收后会产生碎片内存,而Mark-Compact在清除前会进行一步整理,将存活对象向一侧移动,随后清空边界的另一侧内存,这样空闲的内存都是连续的,但是带来的问题就是速度会慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact两者共同进行管理的。由于Mark-Conpact需要移动对象,所以它的执行速度不可能很快,在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时,才使用Mark-Compact。


3.javascript 内存泄露

3.1 全局变量引起的内存泄漏

3.2 闭包引起的内存泄漏

3.3 dom清空或删除时,事件未清除导致的内存泄漏

3.4 子元素存在引用引起的内存泄漏

 

detached-nodes.gif

  • 黄色是指直接被 js变量所引用,在内存里
  • 红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的
  • 子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除

作者:echozzh
链接:https://www.jianshu.com/p/18532079bc2a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!