浅入浅出JS垃圾回收机制
前置概念
再讲垃圾回收机制之前需要明白两个知识点:
- 原始值和引用值
ECMASCRIPT变量可以包含两种不同的类型数据:原始值和引用值。
原始值就是最简单的数据,有六种:Undefined、Null、Boolean、Number、String和Symbol。
保存原始值得变量是按值访问的,因为我们操作的就是存储在变量中的实际值。原始值保存在堆内存上。
引用值是保存在内存中的对象。JS不允许直接访问内存的位置,因此就不能直接操作对象所在的内存空间。在操作对象时实际上是操作对该对象的引用而非实际的对象本身。 - 执行上下文
变量和函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都会关联一个变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问,但后台处理时会用到。
全局上下文是最外层的上下文。在浏览器中全局上下文就是我们常说的window对象。
每个函数都有自己的上下文。当代码执行流进入函数后,函数的上下文会被推到一个上下文栈上,在函数执行完毕之后,上下文栈就会弹出该函数的上下文,将控制权返还给之前的执行上下文。ECMASCRIPT程序的执行流就是通过上下文栈进行控制的。
上下文中的代码执行的时候,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。当前在执行的上下文的变量对象始终位于作用域链的最前端。全局上下文的变量对象始终是作用域链的最后一个变量对象。
代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)
垃圾回收
JS是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。基本思路很简单:确定哪个变量不会再使用,然后释放它所占用的内存。这个过程是周期性的,垃圾回收程序每隔一段时间就会自动执行。如何标记不在使用的变量也有许多不同的实现方式,在浏览器发展历史上,用到过两种主要的标记策略:标记清理和引用计数。
标记清理
当变量进入和离开上下文时,都会被加上相应的标记。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。给变量加标记的方式有很多,但是标记的过程其实不重要,关键是垃圾回收的策略。
引用计数
引用计数的思路就是,声明一个变量并给它赋一个引用值,这样它的引用数就为1。当被赋值的变量又被赋值给另一个变量引用数又会加1。当引用的值被覆盖,引用数就会减1。当引用数为0,就可以对该变量进行回收操作。但是该标记策略,当变量循环引用就会导致引用数一直为2。导致大量内存不被释放,此外该策略也有其他很问题,最终被淘汰。
性能
垃圾回收的时机很重要。如果频繁的触发垃圾回收程序,反倒会增加开销。
开发人员注意事项
在使用垃圾回收的编程环境中,开发者通常无需关心内存管理。将内存占用量保持在一个较小的值可以让性能更好。故总结了以下几点:
-
解除引用
在超出函数上下文时,引用值会被自动解除。然而对于全局上下文中的引用值,我们可以显示的把值置为null,然后他就可以在下次垃圾回收时被回收。 -
使用let和const代替var
这两个关键字不仅能改善代码风格,此外这两个关键字都是块级作用域,可以让垃圾回收程序更早介入。 -
隐藏类和删除操作
如果你的代码非常注重性能,这点可能对你有所帮助。在Chrome浏览器中,使用了V8 JavaScript引擎。在运行期间,V8会将创建的对象域隐藏类关联起来,以跟踪他们的属性特征。能够共用一个隐藏类的对象性能会更好。在我们对实例进行删除或者添加属性时,此时就回去关联不同的隐藏类。
function Person(){
this.name="Mercy";
this.phoneNumber="12312341234";
}
let a=new Person();
let b=new Person();//此时a、b两个实例关联同一个隐藏类
a.age=22;
delete a.phoneNumber;//在这两种情况下,两个个实例就会关联不同隐藏类;
最佳实践就是把不想要的属性置为null,尽量避免新增属性,做到“先建再补充”。
-
避免内存泄漏
这里主要注意两点,及时定时器的清理和意外声明全局变量。 -
静态分配(极端形式)
当我们创建数组时,可以给数组分配足够的大小,因为js的数组大小是动态可变的,当数组大小不够时,js引擎会删除原有数组,再新建一个数组。然后就可能会导致垃圾回收提前。在初期开发中不需要考虑静态分配,这就属于过早优化。