JavaScript:垃圾回收

JavaScript的内存管理由垃圾回收程序自动处理,主要策略包括标记清理和引用计数。标记清理是常用策略,通过标记变量是否在上下文使用来决定回收。引用计数因循环引用问题而不常用,可能导致内存泄漏。性能方面,频繁的垃圾回收会影响性能,现代引擎会动态调整回收策略。开发者可通过解除引用等手段优化内存占用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JavaScript 是使用垃圾回收的语言。

由执行环境负责在代码执行时管理内存。

垃圾回收 指的是使用垃圾回收程序来实现自动的内存管理,即实现自动的内存分配和闲置资源的回收。

垃圾回收的基本思路:

确定某个变量不会再被使用后,就释放该变量占用的内存。

垃圾回收是周期性的,即垃圾回收程序每隔一段时间(或者说在代码执行过程中某个预定的收集时间)就会运行一次。

垃圾回收的过程是一个近似的、不完美的方案,因为某块内存是否还会被使用,属于“不可判定”的问题,这意味着靠算法是解决不了的。

垃圾回收程序必须记录跟踪变量是否能够被回收,以便能够及时回收内存。

即确定哪些变量还会被使用,哪些变量不会再被使用。


主要参考资料:

  • 《JavaScript 高级程序设计(第4版)》- P94(119/931)

垃圾回收策略

如何记录跟踪不会再被使用的变量有许多的具体实现方式。

在浏览器的发展史上,使用过两个主要的垃圾回收策略:标记清理和引用计数。


标记清理

标记清理是 JavaScript 最常用的垃圾回收策略。

标记清理通过为变量添加标记,来记录跟踪变量是否能够被回收。

从逻辑上讲,在上下文中的变量,在上下文销毁之前不应该释放变量的内存。

所以,当变量进入上下文时,可以为变量添加一个 “存在于当前上下文” 的标记。当变量离开上下文时,为变量添加一个 “离开当前上下文” 的标记。

标记清理的大概过程:

垃圾回收程序运行时,会标记内存中存储的所有变量。然后取消所有在上下文中的变量、被在上下文中的变量引用的变量的标记。之后根据某些情况再次标记某些变量。如此垃圾回收程序执行回收时,可以回收被标记的变量的内存。

标记的具体的实现方式有很多种。(不展开叙述)


引用计数

引用计数通过记录每个值被引用的次数,来记录跟踪变量是否能够被回收。

当声明一个变量并给它赋值为一个引用值时,这个被引用的值的引用数为 1 。当一个新的变量引用这个值时,这个值的引用数加 1 。相应的,当引用这个值的变量不再引用这个值时,这个值的引用数减 1 。当一个值的引用次数为 0 时,表示可以回收这个值。

但引用计数不能很好地处理变量之间的循环引用。

当对象 a 、对象 b 相互引用对方作为自己的属性时,就在对象 a 和对象 b 之间形成了一个 循环引用

示例:

  • 循环引用。
    let objA = new Object()
    let objB = new Object()
    
    // 循环引用
    objA.b = objB
    objB.a = objA
    

此时即使外部没有变量引用它们的值,它们的值的引用数依然为 2 ,即对象 a 被引用自身的变量、对象 b 的属性引用。

这就对取消对值的引用的顺序有了要求:

  • 如果想要使形成循环引用的对象 a 、对象 b 的引用数为 0 ,就必须先各自取消自身属性对对方的引用,然后再各自取消引用自身的变量对自身的引用。

    示例:

    let objA = new Object()
    let objB = new Object()
    
    objA.b = objB
    objB.a = objA
    
    // 先各自取消自身属性对对方的引用
    objA.b = null
    objB.a = null
    
    // 再取消各自的变量对自身的引用
    objA = null
    objB = null
    
  • 如果在取消自身属性对对方的引用之前,取消引用自身的变量对自身的引用,会导致之后无法获取自身属性,从而导致无法进行取消自身属性对对方的引用的操作,最终导致在程序运行期间,对方的引用数的最小值永远为 1 。

    示例:

    let objA = new Object()  // 将此时变量 objA 引用的值记为对象 a 
    let objB = new Object()  // 将此时变量 objB 引用的值记为对象 b 
    
    objA.b = objB
    objB.a = objA
    
    // 在取消对象 a 的属性对对象 b 的引用之前,取消变量 objA 对对象 a 的引用
    objA = null
    
    objB.a = null
    objB = null
    
    // 此时 objA 为 null ,无法获取对象 a ,所以无法取消对象 a 的属性对对象 b 的引用
    

在实践的开发中,开发者有很大的可能会没有发现(或者不知道)代码中存在循环引用,这在使用引用计数实现垃圾回收的编程环境中,会很容易造成内存泄漏。

所以很多时候,不使用引用计数来实现垃圾回收。

如果使用引用计数来实现垃圾回收,则需要针对循环引用进行改进。(不展开描述)


性能

运行垃圾回收程序必然需要占用一些计算资源,从而消耗一些性能。

垃圾回收程序会周期性运行,而回收内存的时机和频率,是一个需要重视的性能问题:

  • 如果高频地执行内存回收,必然会导致大量的计算资源被垃圾回收程序占用。
  • 如果低频地执行内存回收,会使能够被回收的内存得不到及时的回收,可能会导致大量的暂时性内存泄漏。

开发者不知道在程序运行时,什么时候会进行垃圾回收,因此最好的办法是,在编写代码时应该保证,当进行垃圾回收时,垃圾回收程序能够回收那些应该回收的内存。

现代垃圾回收程序会基于对 JavaScript 运行时的环境的探测来决定何时回收内存。

不同的 JavaScript 引擎可能有不同的探测机制,但基本是根据已分配对象的内存大小和数量来判断的。

有些浏览器会提供主动触发垃圾回收的 API (不推荐使用),例如:IE、Opera7+


(了解)IE 的性能解决方案

IE 早期的垃圾回收的性能解决方案是:

根据内存分配数决定是否执行垃圾回收,比如分配了 256 个变量后,垃圾回收程序就会执行垃圾回收。

但问题在于,可能某个程序在运行时始终需要那么多的变量,这时,垃圾回收程序就会高频地执行垃圾回收,对性能造成严重的影响。

IE7 发布后,更新了垃圾回收程序,改善了垃圾回收的性能解决方案:动态改变内存分配的阈值。

内存分配数在开始时为起始阈值。

如果执行垃圾回收后,回收的内存不到已分配的 15% ,内存分配的阈值将会翻倍。

如果执行垃圾回收后,回收的内存达到已分配的 85% ,则内存分配的阈值将会被重置为起始阈值。

如此修改,极大地提升了重度依赖 JavaScript 的网页在 IE 中的性能。


内存管理

在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。

不过,JavaScript 运行在一个内存管理和垃圾回收都很特殊的环境。

分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动浏览器的内存更少。

这更多是出于安全考虑,避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。

这个内存分配限制会影响下面的行为:

  • 变量的分配。
  • 调用栈的大小。
  • 在线程中同时执行的语句数量。

将内存占用量保持在一个较小的值可以让页面的性能更好。


解除引用

优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。

如果数据不再需要,可以 解除引用 ,即将相应的变量设置为 null ,从而释放对当前数据的引用。

这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用。

解除引用的关键在于确保相关的值已不再被当前上下文使用,以便在下次垃圾回收时能够回收内存。


根据 JavaScript 引擎优化编码

由于不同的 JavaScript 引擎会有不同的具体实现,有时候需要根据浏览器使用的 JavaScript 引擎来采取不同的性能优化策略。

比如下面这种针对 V8 JavaScript 引擎的编码优化。

V8 在将解释后的 JavaScript 代码编译为实际的机器代码时会使用“隐藏类”。

在运行期间,V8 会将使用相同的构造函数创建的多个对象实例与一个隐藏类关联起来,以跟踪它们的属性特性,这些对象实例会共享隐藏类。V8 会针对这种情况进行优化。

但 V8 不能保证,在任何情况下,使用相同的构造函数创建的多个对象实例都会共享一个隐藏类。

当使用相同的构造函数创建的多个对象实例后,为某个对象实例动态地添加 / 删除属性,此时该对象实例将与另一个隐藏类关联,不与其它的对象实例共用一个隐藏类。

解决方案很简单,就是不要为对象动态添加 / 删除属性,即不要使用 JavaScript 的“先创建再补充(ready-fire-aim,再完善)”式的对象使用方式。

所以建议在构造函数中一次性声明实例对象的所有属性。


静态分配与对象池

为了提升 Javascript 的性能,最后要考虑的一点往往就是压榨浏览器。

此时一个关键的问题就是如何减少浏览器执行不必要的垃圾回收的次数。

开发者无法直接控制什么时候开始执行垃圾回收,但会间接触发垃圾回收的执行。

不必要的垃圾回收指的是:

当前代码的执行情况满足了垃圾回收的执行条件,触发了垃圾回收的执行。

但是所有的内存都正在被使用,没有可以被回收的内存。

浏览器决定何时执行垃圾回收的一个标准是对象更替的速度。

对象更替指的是:

为对象分配内存以创建对象,然后对象等待被回收,最后对象的内存被回收的过程。

如果在短时间内提高对象更替的速度,但在这之后将长时间保持正常的、对象更替的速度,这会导致不必要的垃圾回收的产生。

在短时间内提高对象的更替速度,这会“刺激”浏览器采取“激进”的垃圾回收策略,即提高垃圾回收的执行频率,并保持一定的惯性,需要一定的时间才会恢复到“平和”的垃圾回收策略,即正常的垃圾回收的执行频率。

但在浏览器恢复到“平和”的垃圾回收策略之前,代码的执行的、对象更替的速度已经恢复到正常的状态。此时浏览器任然采取“激进”的垃圾回收策略,这必然会造成不必要的垃圾回收,一直持续到浏览器恢复到“平和”的垃圾回收策略。在这期间,必然会影响性能。

示例:

  • 短时间内提高对象更替的速度。
    class Entity {
    	constructor(data = 'default data') {
    		this.data = data
    	}
    }
    
    function combineEntity(entity_01, entity_02) {
    	let entity = new Entity()  // 为对象分配内存以创建 Entity 的实例对象 
    	entity.data = entity_01.data + '_' + entity_02.data
    	
    	return entity  // 调用函数时,函数返回值后,变量 entity 的内存等待被回收
    }
    
    const entity_01 = new Entity('01')
    const entity_02 = new Entity('02')
    
    let combinedEntity = null
    
    for(let i = 0; i < 100; i++) {
    	combinedEntity = combineEntity(entity_01, entity_02)
    	console.log('count of invoking combineEntity():', i + 1)
    	console.log('ccombinedEntity:', combinedEntity)
    }  // 频繁调用函数 combineEntity() 100 次
    

解决思路是避免频繁地动态创建对象。

其中一个方案是使用对象池,以静态分配对象替代动态创建对象,减少动态创建对象的次数。

对象池 是一种编程方式。

对象池拥有一定数量的对象实例,对象池中的对象用于被外部使用,对象的使用权可以被分配和被释放。

示例:

  • 使用对象池的静态分配对象替代动态创建对象,减少动态创建对象的次数。
    // 简单的对象池实现
    class ObjectPool {
    	constructor(objectCount) {
    		// 创建对象
            this.objects = new Array(objectCount).fill(null).map(
    	        (_, index) => {
    		        return {
    	                isFree: true,
    	                data: 'data_' + index
    	            }
    	        }
            )
    	}
    	
    	allocate() {
    		return this.objects.find(
    			(object) => {
    				if(object.isFree) {
    					object.isFree = false
    					return true
    				}
    				return false
    			}
    		)
    	}  // 分配对象
    	
    	free(needFreedObject) {
    		this.objects.find(
    			(object) => {
    				if(needFreedObject === object) {
    					object.isFree = true
    					return true
    				}
    				return false
    			}
    		)
    	}  // 释放对象
    }
    
    class Entity {
    	constructor(data = 'default data') {
    		this.data = data
    	}
    }
    
    function combineEntity(entity_01, entity_02, entityObject) {
    	entityObject.data = entity_01.data + '_' + entity_02.data
    	return entityObject
    }  // 不再动态创建对象
    
    const objectPool = new ObjectPool(3)  // 创建对象池
    
    let entity_01 = null,
    	entity_02 = null,
    	entityObject = null,
    	combinedEntity = null
    
    for(let i = 0; i < 100; i++) {
    	entity_01 = objectPool.allocate()  // 分配对象池中的对象
    	entity_02 = objectPool.allocate()
    	entityObject = objectPool.allocate()
    	
        console.log('entity_01:', entity_01)
        console.log('entity_02:', entity_02)
        console.log('entityObject:', entityObject)
        
    	entity_01.data = 'entity_01'  // 填充自定义数据
    	entity_02.data = 'entity_02'
    	
    	combinedEntity = combineEntity(entity_01, entity_02, entityObject)
    	console.log('count of invoking combineEntity():', i + 1)
    	console.log('combinedEntity:', combinedEntity)
    	
    	objectPool.free(entity_01)  // 释放对象池中的对象
    	objectPool.free(entity_02)
    	objectPool.free(entityObject)
    }
    
    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值