JavaScript是一种弱类型的、动态的语言。弱类型(支持隐式类型转换)意味着运行代码时,JS引擎可以自己计算出数据类型。动态的(运行时做类型检测,不需要提前声明数据类型,这也导致性能低于静态语言)代表着同一变量可以保存不同类型数据。
JS执行过程中,主要有三中类型内存空间:代码空间(存储可执行代码)、栈空间(调用栈,存储执行上下文)和堆空间(保存引用类型数据)。本文主要讨论栈空间和堆空间,栈空间主要存储占内存较小的原始数据,其大小有限,以保证上下文切换的效率和程序执行效率;堆空间很大,往往存放引用类型数据,并通过引用和变量关联(字符串,symbol,bigint虽然是原始类型,但是实际还是存放在堆空间的)。
JS中声明是并不会分配内存,需要执行代码赋值后,根据值的类型在栈或者堆中分配内存。
我们知道函数执行的时候会先编译,并创建一个空的执行上下文;编译过程中,遇到内部函数会对其进行一次快速的词法扫描,如果发现内部函数引用了外部函数的变量,javaScript引擎会判断出这是一个闭包;于是在堆空间创建一个"closure(外部函数)"的对象(此为内部对象,javaScript无法访问)。综上外部函数执行时产生闭包,其执行完毕后调用栈弹出执行上下文,但closure(外部函数)函数的引用依然存在(外部函数执行上下文弹出后,内部函数返回对象中包含对堆空间closure(外部函数)的引用),并包含在后续新创建的执行上下文中。[1]
现代浏览器中闭包通常不会造成内存泄漏,没有被引用的闭包会被自动回收,不过如果没用的闭包还保存在全局变量中,依然会内存泄漏!
不同语言的垃圾数据回收策略分为手动回收和自动回收。手动回收策略,将由代码控制内存分配和销毁,例如C和C++;使用自动回收方式的语言则是由垃圾回收器来释放垃圾数据占有的内存空间,例如JAVA、javaScript、Python等。JavaScript内存中数据存储分为栈空间和堆空间,他们采用的回收机制并不相同。
调用栈中主要存储函数的执行上下文,当函数执行完成后需要销毁其对应的执行上下文。这个过程主要借助ESP,也就是记录当前执行状态的指针。当函数A执行完成,ESP下移到调用其的函数B,此时A的执行上下文虽然保存在栈内存,但已经是无效内存,会在B调用其他函数时被覆盖。
堆空间的垃圾回收,就需要用到垃圾回收器了,不同的垃圾回收器可选用的垃圾回收算法很多,往往需要根据对象生存周期的不同使用不同的算法。再V8中会将堆分为新生代和老生代两个区域,新生代存放生存时间短的对象,老生代存放生存时间久的对象;前者通常只有1~8M的容量,后者则大的多。结合新生代和老生代的区域划分,V8分别使用副垃圾回收器和主垃圾回收器两个不同的垃圾回收器,以便更高效的实现垃圾回收。
不同的垃圾回收器虽然采用了不同的垃圾回收算法,但是他们的执行流程大致一致。
首先标记空间中的活动对象和非活动对象,前者仍在使用,后者则可以进行垃圾回收;
然后将回收非活动对象所占据的内存,将内存中所有标记为可回收的对象统一清理;
最后进行内存整理;通常频繁回收对象后,内存中会存在大量不连续的内存碎片,这会导致分配连续较大内存时出现内存不足,故需要进一步整理内存空间的内存碎片。
并不是所有垃圾回收器都会产生内存碎片,例如副垃圾回收器,所以内存整理并不一定会进行。
上面我们知道新生代使用副垃圾回收器,此区域存放大多数小的对象并且空间不大,所以垃圾回收很频繁(频率高于主垃圾回收器)。副垃圾回收器使用Scanvenge算法,将新生代空间对半划分为对象区域和空闲区域,前者存放对象,并在快被写满时,执行一次垃圾清理,具体过程如下:
首先对象区域中进行垃圾标记;完成后进入垃圾清理阶段,此时存活的对象会被复制到空闲区域并有序排列(复制过程也相当于完成内存整理);完成复制后,对象区域和空闲区域进行角色反转,原有空闲区域成为对象区域,如此重复此过程。
复制操作需要时间成本,故新生代空间设置较小,以保证执行效率;同时也由于新生代空间限制容易装满,JS引擎采用了对象晋升策略,将两次垃圾回收依然存活的对象移动到老生代。
由于老生代存放的是新生代晋升的对象,以及一些较大对象,它们存活周期长或者占用空间大,并不适合使用Scanvenge算法。主垃圾回收器采用标记-清除(Mark-Sweep)进行垃圾回收。首先,在标记阶段从根元素开始递归遍历,能到达的为活动对象,无法到达的元素判断为垃圾数据。然后,垃圾清除过程,直接清除掉垃圾数据,此过程会产生大量内存碎片影响内存分配。最后,采用标记-整理(Mark-Compact)算法,将所有活动对象向一端移动,并清理掉端边界之外的内存。[2]
由于JS运行在主线程,当垃圾回收算法执行,会暂停JS脚本等待垃圾回收结束执行,出现全停顿(Stop- The-World)。新生代空间小,影响较小;老生代则需要借助增量标记算法(Incremental Marking)将标记过程分为多个子标志过程和JS应用交替执行。
[1] 栈空间和堆空间:数据是如何存储的?https://time.geekbang.org/column/article/129596
[2] 垃圾回收:垃圾数据是如何自动回收的?https://time.geekbang.org/column/article/131233