浅谈垃圾回收
1 垃圾回收是什么
- 背景:javaScript语言是使用垃圾回收的语言,也就是说指向环境负责在代码执行管理时管理内存。在c和c++语言中,跟踪内存使用对开发者来说是个很大的负担。所以javaScript通过自动内存管理实现内存分配和闲置资源回收,不再把内存回收的权利交给开发者,从而减轻开发者的负担
- 实现基本思路:确定哪个变量不使用,就对哪个变量进行回收。这个过程是周期性的,在一定的时间段内执行这个过程。
- 问题:大致思路容易实现,但会遇到问题,因为内存的使用,是属于“不可预定的”,通过算法也不一定能解决
- 标记策略:主要分两种,一种是标记清理,一种是引用计数
标记清理
- 标记清理是 javascript 中最常用的一种垃圾回收策略。当我们的变量进入上下文,比如在函数内部声明一个变量,这个变量会被加上存在上下文中的标记。在上下文中的变量,从逻辑上讲,这个变量所占用的内存永远就不要清理。因为只要上下文中的代码在运行,这个变量就有可能会被用到,当我们的变量离开上下文时,也会被加上离开上下文的标记
- 标记的方法有很多种,这个方法并不是唯一的。可以将变量分为“在上下文中的变量列表”和不在上下文中的变量列表“,在变量进入或者离开上下文时,将它加入到对应的变量列表
- 标记的方法有很多种,这个方法并不是唯一的。可以将变量分为“在上下文中的变量列表”和不在上下文中的变量列表“,在变量进入或者离开上下文时,将它加入到对应的变量列表
- 在运行垃圾回收程序的时候,会标记内存中的所有变量。然后将所有上下文中的变量和被上下文中引用的变量的标记去掉,在进行这个过程后,剩下的都是有标记的变量(待删除),原因是在任何上下文中都访问不到这些变量了,最后将这些变量删除。
- 每个浏览器都有自己实现的一套标记清理方法,只是运行垃圾回收运行的频率有所区别。
引用计数
- 引用计数的垃圾回收策略相比于标记清理来说没有那么常用,但是也是实现回收的一种策略。
- 实现思路是将每个值都记录它被应用的次数。当一个值被变量引用时,这个值的引用次数加 1,这个值如果又被另外一个变量引用,引用次数也会加 1,如果引用该值的变量被其他值覆盖了,那么引用次数加 1。当垃圾回收程序运行的时候,去看内存中每个变量的引用次数,如果为 0,就会将这个值所占的内存释放。
- 使用引用计数会存在一个严重的问题,循环引用:
function problem() {
let objectA = new Object()
let objectB = new Object()
objectA.b = objcetB
objectB.a = objcetA
}
在这个例子中,objectA 和 objcetB 的引用次数都是 2,在标记清理策略下,函数执行完,这两个变量值所占的内存会被清理。而在引用计数的策略下,onjectA 和 objectB 在函数结束后还会存在,因为它们的引用次数永远不会为 0。如果这个函数多次被调用,就会造成大量的内存不会被释放,造成内存泄漏。因为这个弊端,现在很少有使用引用计数的策略来进行垃圾回收。
性能
垃圾回收程序会周期性的运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度尤为重要。这个在移动设上尤其重要,因为移动设备的内存有限。如果频次高的进行垃圾回收,会明显拖慢渲染的速度和帧率。对我们开发者而言,最好就是手动将一些不需要使用的变量,赋值为 null,让垃圾回收程序提早运行。
现代垃圾回收程序会基于对 javascript 运行环境的探测来决定何时运行。探测机制因引擎而异。v8 团队在 2016 年一篇博文的说法:“在一次完整的垃圾回收之后,v8 的增长策略会根据活跃对象的数量增加一些余量来确定何时再次进行垃圾回收。
由于垃圾调度回收程序方面的问题会导致性能下降,IE 曾饱受诟病(包括现在~~)。它的策略就是是根据变量的分配数。比如分配了 256 个变量,4096 个对象/数组/字面量和数组槽位(slot),或者 64KB 字符串。只要满足其中一个条件,就会运行垃圾回收程序。这样实现就会出现一个问题,如果一个脚本所需要的变量一直满足这个条件,那么垃圾回收程序就会一直运行,显然这会很影响性能。
IE7 发布后,javascript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。IE7 与 IE6 的起始阈值相同,如果垃圾回收程序回收的内存达不到内存的 15%,这些阈值就会翻倍。如果有一次垃圾回收的程序已经达到回收是 85%,则这个阈值重置为初始值。
其实有些浏览器是支持主动触发垃圾回收的(但是不推荐这么做),在 IE 中,window.CollectGarbage()方法会触发垃圾回收。在 Opera7 及更高的版本中,window.opera.collect()也会启动垃圾回收程序。
内存管理
在使用垃圾回收的编程环境中,作为开发者的我们是不需要关心内存管理。但是,javascript运行在一个内存管理和垃圾回收都很特殊的环境。浏览器分配的内存是很少的,相对于桌面,更何况移动端的浏览器。给浏览器分配少的内存是处于安全考虑的,如果浏览器运行javascript使用过多内存,可能会导致系统崩溃,这是十分危险的行为。
虽然系统上一级做了限制,我们平时在开发时,也应该让内存占用量保持在一个较小的值,手动回收不要的变量是很关键的。ES6新增的两个关键字let和const不仅有助于改善代码风格,还能改进垃圾回调的过程。因为let和const只在块级作用域内有效,如果块级作用域比函数作用域更早终止的情况下,垃圾回收程序就能更早的介入,从而减少内存使用量。
内存泄漏:内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。在我们日常开发中,其实是很容易造成内存泄漏,比如直接声明一个全局变量,不使用任何关键字。声明的这个变量会一直存在,除非windows对象被清理调。其实避免这种问题很简单,只要每次声明变量时,加上var,let,const关键字即可,加上这些关键字后,在对应的函数执行完后就会得到清理。还有就是使用定时器,定时器里面的回调函数引用变量,只要定时器还在运行,这个变量就一直不会清理。还有就是闭包,闭包会将本来要清理的变量保存下来,直到返回的闭包函数销毁调,这个变量才会被清理掉。
结语
本文主要是分享了垃圾回收机制的原理,主要是看完《JavaScript高级程序设计(第4版)》该书的一些分享。