JS垃圾回收机制
垃圾回收机制
GC 即 Garbage Collection .所谓的垃圾就是内存不会再被使用了,那么这些内存就应该被垃圾回收机制释放掉
基于两个原理:
1.考虑某个变量或者对象在未来程序运行期间不会被使用
2.向这些对象要求归还空间
垃圾回收机制方法
1.标记清除法
过程:
1.垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
2.然后从各个根对象开始遍历,把能够访问到的变量置1
3.清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
4.最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点:
实现比较简单,只存在打标记和不打标记两种情况,这种情况使得可以采用二进制01来进行标记
缺点:
内存碎片化: 这样内存里面空闲的位置不是连续的,这样就可以使用操作系统学到的方法来分配内存了,比如先什么最先适配、最优适配、最坏适配…
2.引用计数法
过程:
1.当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1
2.当这个变量指向其他一个时该值的引用次数便减1
3.当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
缺点:
循环引用:会导致内存泄漏
计数器需要占很大位置。
内存泄漏
定义:
当不再用到的对象内存,没有及时被回收或者无法被回收就会导致内存泄漏
常见的内存泄漏:
1.全局变量
解决办法:在函数内使用严格模式,严格模式下禁止this关键字指向window
2.定时器或者回调函数
当定时器没有被及时清除会导致内存泄漏
3.闭包
解决办法:手动清除,置为null
4.没有清除dom的引用
let node = document.getbyid(‘a’)
然后把a节点删除了,但是它的引用还没删除,也就是let node
解决办法 node = null
Chrome V8垃圾回收机制
说到栈内的内存,操作系统会自动进行内存分配和内存释放,而堆中的内存,由JS引擎(如Chrome的V8)手动进行释放,当我们的代码没有按照正确的写法时,会使得JS引擎的垃圾回收机制无法正确的对内存进行释放(内存泄露),从而使得浏览器占用的内存不断增加,进而导致JavaScript和应用、操作系统性能下降。
基本概念
在JavaScript中,其实绝大多数的对象存活周期都很短,大部分在经过一次的垃圾回收之后,内存就会被释放掉,而少部分的对象存活周期将会很长,一直是活跃的对象,不需要被回收。为了提高回收效率,V8 将堆分为两类新生代和老生代,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
- 副垃圾回收器 - Scavenge:主要负责新生代的垃圾回收。
- 主垃圾回收器 - Mark-Sweep & Mark-Compact:主要负责老生代的垃圾回收。
新生代垃圾回收器 - Scavenge
在JavaScript中,任何对象的声明分配到的内存,将会先被放置在新生代中,而因为大部分对象在内存中存活的周期很短,所以需要一个效率非常高的算法。在新生代中,主要使用Scavenge算法进行垃圾回收。
Scavenge算法将新生堆一分为二,一个是处于使用状态的空间我们暂且称之为使用区,一个是处于闲置状态的空间我们称之为空闲区。
工作方式可简述为:
- 新加入的对象都会放入使用区
- 对使用区的存活对象作标记
- 标记完成之后将使用区的存活活动对象复制进空闲区并进行排序
- 随后进入垃圾清理阶段,即将非存活对象占用的空间清理掉。也就是将使用区的非活动对象内存进行清除。
- 最后将使用区与空闲区进行对换。
老生代垃圾回收 - Mark-Sweep & Mark-Compact
新生代空间中的对象满足一定条件后,晋升到老生代空间中,在老生代空间中的对象都已经至少经历过一次或者多次的回收所以它们的存活概率会更大,如果这个时候再使用scavenge算法的话,会出现效率低下和资源浪费的问题。所以使用Mark-Sweep 和 Mark-Compact算法来代替。
Mark-Sweep
Mark-Sweep处理时分为两阶段,标记阶段和清理阶段,看起来与Scavenge类似,不同的是,Scavenge算法是复制活动对象,而由于在老生代中活动对象占大多数,所以Mark-Sweep在标记了活动对象和非活动对象之后,直接把非活动对象清除。
标记阶段:对老生代进行第一次扫描,标记活动对象
清理阶段:对老生代进行第二次扫描,清除未被标记的对象,即清理非活动对象
这样其实会导致一个问题,就是被清除的对象会产生很多内存碎布分布在各个内存地址。
所以我们才有另一种算法 Mark-Compact 来解决这些内存碎片。
Mark-Compact
由上文可知,Mark-Sweep完成后,会存在大量的内存碎片,若不清理掉这些碎片的话,如果需要分配一个大对象,这里的碎片都无法完成分配,会提前触发垃圾回收机制。
为了解决内存碎片问题,Mark-Compact被提出,它是在 Mark-Sweep的基础上演进而来的,相比Mark-Sweep,Mark-Compact添加了活动对象整理阶段,将所有的活动对象往一端移动,移动完成后,直接清理掉边界外的内存。
并行回收
全停顿
JavaScript 是一门单线程的语言,它是运行在主线程上的,那在进行垃圾回收时就会阻塞 JavaScript 脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,我们把这种行为叫做全停顿。
那么就是每次垃圾回收时间多少秒,我们的逻辑就会停顿多少秒,如果垃圾回收时间过长,就会造成页面卡顿的现象。
并行
并行式垃圾回收允许主线程和辅助线程同时执行同样的工作,这样可以让辅助线程来分担主线程的工作,使得垃圾回收所耗费的时间等于总时间除以参与的线程数量。
新生代对象空间就采用并行策略,在执行垃圾回收的过程中,会启动了多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域,这个过程中由于数据地址会发生改变,所以还需要同步更新引用这些对象的指针,此即并行回收。
增量标记
为了减少全停顿的时间,在 2011 年,V8 对老生代的标记进行了优化,从全停顿标记切换到增量标记
增量
增量就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记。
假如我们在一次完整的 GC 标记分块暂停后,执行任务程序时内存中标记好的对象引用关系被修改了又怎么办呢
在查阅资料后,V8 对这两个问题对应的解决方案分别是三色标记法与写屏障。由于自己不是很理解这两种解决方案,所以在此不作过多的解释,若感兴趣的话,大家可以查阅相应资料。
懒性清理
增量标记其实只是对活动对象和非活动对象进行标记,对于真正的清理释放内存 V8 采用的是惰性清理。
懒性清理其实和常用的懒加载一样。都是为了让过程延迟执行。
增量标记完成后,惰性清理就开始了。当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理过程稍微延迟一下,让 JavaScript 脚本代码先执行,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记。
最后,懒性清理也是有缺陷的。由于每个小的增量标价之间执行了JavaScript代码,堆中的对象指针可能发生了变化,需要使用写屏障技术来记录这些引用关系的变化
缺点:
首先是并没有减少主线程的总暂停的时间,甚至会略微增加。
由于写屏障机制的成本,增量标记可能会降低应用程序的吞吐量