JavaScript垃圾回收机制

2021-05-31 23:02

阅读:473

标签:fun   提升   编程   rgba   color   共享   应用   访问   存在   

JavaScript垃圾回收机制

JavaScript使用垃圾自动回收机制进行内存管理,无需程序员手动分配和释放内存。垃圾回收的基本思路是确定哪些变量不会再次被使用,然后回收这些变量占用的内存。垃圾回收机制会影响应用程序的性能,因此它应该是周期性的,垃圾回收程序每隔一段时间会运行一次。常用的垃圾回收机制主要包括标记清除和引用计数。

 

1.标记清除

标记清除是最常用的JS垃圾回收机制。当程序的执行流进入到一个函数中时,系统会为这个函数创建执行上下文和与其执行上下文关联的活动对象。执行上下文决定了这个函数可以访问的变量及其行为,而能被这个函数访问和执行的变量与函数都位于活动对象上。位于活动对象上的变量应该被添加“存在于执行上下文”的标记,这是因为它们可能被使用。而当函数执行完毕,活动对象上的变量应该被添加“离开执行上下文”的标记,这代表它们不会被再次使用,应该被垃圾回收程序回收。当然,标记变量的方式不止这一种,我们也可以维护两个列表,分别表示存在于执行上下文和离开执行上下文的变量,当变量不会被使用时就将其移动到另一个列表。垃圾回收程序在每次运行时将销毁所有离开执行上下文的变量并回收其占用的 空间。

“垃圾回收程序运行的时候,会标记内存中存储的所有变量(记住,标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做次内存清理,销毁带标记的所有值并收回它们的内存。”  (引自《JavaScript高级程序设计》)
 

2.引用计数

引用计数的核心思想是记录每个值被引用的次数。一个引用值被赋值给一个变量时,其引用数加一,如果再赋给该变量另一个引用值,则前一个值的引用数减一。引用数为0的值没有办法被再次访问,因此可以被垃圾回收程序安全回收。引用计数可能带来循环引用问题,即不同对象的相互引用导致它们都没有办法被回收。

1 function foo() {
2     let obj1 = new Object(), obj2 = new Object();
3     obj1.myObject = obj2;
4     obj2.myObject = obj1;
5 }

在上面的代码中,obj1作为obj2的属性被引用,而obj2又作为obj1的属性被引用,在采用的引用计数策略的环境下,它们的引用数永远不可能为0,因此在函数执行结束之后,这两个对象占用的内存依然无法被回收。解决循环引用的办法是,在确定不会再次使用存在循环引用的变量的情况下,将它们的值设置为null,切断变量与引用值之间的关系,将引用数强制清零。

 

3.垃圾回收程序对性能的影响

垃圾回收程序会周期性运行。如果应用程序中存在很多变量,可能导致垃圾回收程序的执行时间很长,从而降低应用程序的性能。我们无法得知垃圾回收程序运行的时间点,因此需要在编程时确保垃圾回收过程每次都能尽快结束,尽可能降低其对性能的影响。

“现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。”(引自《JavaScript高级程序设计》)
 

4.内存管理

尽管在编写JavaScript程序时我们无需关心内存的分配与回收,但让内存的占用量保持在较低水平可以提高程序的性能。优化内存占用的一个常用方法是在一个变量不会再次被使用时将其设置为null,从而切断其与之前赋给它的引用值的联系,从而让垃圾回收程序可以及时回收内存。这种方法尤其适合全局变量和全局对象的属性,因为局部变量在程序执行流离开其所在上下文时会自动被解除引用。需要注意的是,解除对一个值的引用并不会导致其所占用的内存空间立刻被回收,只是告诉垃圾回收程序这个值不会被再次使用。

此外,在保证程序逻辑正确的前提下尽可能使用let和const声明变量和常量也可以提升程序性能。使用let和const声明的变量和常量拥有块级作用域,而块级作用域通常比函数作用域更早结束,从而使不会被再次使用的值尽早被回收。

另一个需要注意的问题是V8引擎的隐藏类。在程序运行期间,V8会将每个对象与一个隐藏类关联起来,如果多个对象拥有相同的属性(但属性的值不一定要相同),那么它们将共享同一个隐藏类。

1 function Person(name, age) {
2     this.name = name;
3     this.age = age;
4 }
5 
6 let person1 = new Person("Alex", 18);
7 let person2 = new Person("Apple", 20);

在上面的代码中,person1和person2拥有相同的属性(相同的构造函数和原型),因此它们将共享同一个隐藏类。但是,如果之后为person1添加或者删除一个属性都将导致它们不再共享隐藏类,V8将创建一个新的与person1关联的隐藏类。如果程序中存在多个共享隐藏类的对象,频繁的增删它们的属性将导致V8频繁地创建新的隐藏类,这将对程序性能造成影响。解决这个问题的方法是,在一开始就尽可能确定一个对象的全部属性,避免在运行时对其属性进行频繁修改。此外,还可以通过将不再需要的属性设置为null而不是删除该属性来避免创建新的隐藏类。

内存泄漏问题同样值得注意。在JS中,在函数中意外地声明全局变量会导致内存泄漏。

1 function foo() {
2     name = "Alex";
3     console.log("Hello " + name);
4 }
5 
6 foo();
7 console.log(global.name); // Alex

在上面的代码中,name被声明为了全局变量,在函数执行完毕之后依然无法回收其所占用的内存,这种问题的解决方法是使用var,let或者const声明局部变量或常量。另外,在使用闭包时也会导致内存泄漏。

1 function foo() {
2     name = "Alex";
3     return function () {
4         console.log("Hello " + name);
5     }
6 }
7 
8 const func = foo();
9 func(); // Hello Alex

在上面的代码中,尽管foo函数已经执行完毕,但其返回的函数依然可以访问其作用域内部的name变量,这是因为foo函数返回的函数依然引用着foo函数的活动对象中的name变量,即foo函数的活动对象依然位于其返回的函数的作用域链上。为了解决这个问题,我们应该在func引用的函数执行完毕之后将其设置为null,解除其对foo的活动对象的引用,从而使得垃圾可以被及时回收。

JavaScript垃圾回收机制

标签:fun   提升   编程   rgba   color   共享   应用   访问   存在   

原文地址:https://www.cnblogs.com/ccpeng/p/14728042.html


评论


亲,登录后才可以留言!