提到垃圾回收机制就不得不提js的数据类型:
基本数据类型:有固定大小,值保存在栈内存中。
引用数据类型:大小不固定,栈内存中存储的只是堆内存的一个指针,指向堆内存中的对象空间。
由于栈内存所存的基本数据类型大小是固定的,所以其内存是操作系统自主分配和释放回收的。但是由于堆内存大小不固定,所以需要js引擎(Chrome的V8)来手动释放这些内存。
1、引用法。(引用计数法)
判断一个对象的引用数,引用数为0
就回收,引用数大于0
就不回收,会存在两个对象相互引用从而造成内存泄漏问题。
2、分代回收。(标记清除,标记整理)
新生代:存在时间短,主要由副垃圾回收器 + Scavenge算法负责。
在新生代中,还进一步进行了细分。分为nursery子代
和intermediate子代
两个区域,一个对象第一次分配内存时会被分配到新生代中的nursery子代
,如果经过下一次垃圾回收这个对象还存在新生代中,这时候我们将此对象移动到intermediate子代
,在经过下一次垃圾回收,如果这个对象还在新生代中,副垃圾回收器会将该对象移动到老生代中,这个移动的过程被称为晋升。
老生代:存在时间长,主要由主垃圾回收器 + Mark-Sweep
算法(标记清除) 和Mark-Compact
算法(标记整理---整理清除后留下的零散空间)
Mark-Sweep(标记清理)
Mark-Sweep
分为两个阶段,标记和清理阶段,之前的Scavenge算法
也有标记和清理,但是Mark-Sweep算法
跟Scavenge算法
的区别是,后者需要复制后再清理,前者不需要,Mark-Sweep
直接标记活动对象和非活动对象之后,就直接执行清理了。
- 标记阶段:对老生代对象进行第一次扫描,对活动对象进行标记
- 清理阶段:对老生代对象进行第二次扫描,清除未标记的对象,即非活动对象
3、全停顿
由于js单线程的特性,js代码执行和垃圾回收同时进行时,垃圾回收会优先于代码执行,等垃圾回收完毕,在执行js代码。这个过程成为全停顿。所以可能会导致页面卡顿, Orinoco优化提出了增量标记、懒性清理、并发、并行
的方法来解决这个问题。
增量标记(Incremental marking):
咱们前面不断强调了先标记,后清除
,而增量标记就是在标记
这个阶段进行了优化。我举个生动的例子:路上有很多垃圾
,害得路人
都走不了路,需要清洁工
打扫干净才能走。前几天路上的垃圾都比较少,所以路人们都等到清洁工全部清理干净才通过,但是后几天垃圾越来越多,清洁工清理的太久了,路人就等不及了,跟清洁工说:“你打扫一段,我就走一段,这样效率高”。
大家把上面例子里,清洁工清理垃圾的过程——标记过程,路人——JS代码
,一一对应就懂了。当垃圾少量时不会做增量标记优化,但是当垃圾达到一定数量时,增量标记就会开启:标记一点,JS代码运行一段
,从而提高效率
惰性清理(Lazy sweeping)
上面说了,增量标记只是针对标记
阶段,而惰性清理就是针对清除
阶段了。在增量标记之后,要进行清理非活动对象的时候,垃圾回收器发现了其实就算是不清理,剩余的空间也足以让JS代码跑起来,所以就延迟了清理
,让JS代码先执行,或者只清理部分垃圾
,而不清理全部。这个优化就叫做惰性清理
整理标记和惰性清理的出现,大大改善了全停顿
现象。但是问题也来了:增量标记是标记一点,JS运行一段
,那如果你前脚刚标记一个对象为活动对象,后脚JS代码就把此对象设置为非活动对象,或者反过来,前脚没有标记一个对象为活动对象,后脚JS代码就把此对象设置为活动对象。总结起来就是:标记和代码执行的穿插,有可能造成对象引用改变,标记错误
现象。这就需要使用写屏障
技术来记录这些引用关系的变化。
并发(Concurrent)
并发式GC允许在在垃圾回收的同时不需要将主线程挂起,两者可以同时进行,只有在个别时候需要短暂停下来让垃圾回收器做一些特殊的操作。但是这种方式也要面对增量回收的问题,就是在垃圾回收过程中,由于JavaScript代码在执行,堆中的对象的引用关系随时可能会变化,所以也要进行写屏障
操作。
并行
并行式GC允许主线程和辅助线程同时执行同样的GC工作,这样可以让辅助线程来分担主线程的GC工作,使得垃圾回收所耗费的时间等于总时间除以参与的线程数量(加上一些同步开销)。