文章目录
- 特点
- 方式
-
- [⚡️1. 标记-清除(Mark-Sweep)](#⚡️1. 标记-清除(Mark-Sweep))
- [2. 分代收集(Generational Collection)](#2. 分代收集(Generational Collection))
- [3. 引用计数(Reference Counting)](#3. 引用计数(Reference Counting))
- 减少垃圾回收
- 内存泄漏
- 触发时机
前端垃圾回收机制是JavaScript引擎自动管理内存的一种方式,主要目的是识别和释放不再使用的内存,以防止内存泄漏。
JavaScript引擎通过特定的垃圾回收算法(如标记-清除、引用计数等)来判断哪些对象已经不再被引用,从而将其占用的内存释放。
前端垃圾回收机制是JavaScript引擎内部实现的一部分,开发者通常无需直接操作。
然而,了解垃圾回收机制的工作原理可以帮助开发者编写更高效、更安全的代码。
特点
-
自动化:前端垃圾回收机制是自动进行的,无需开发者手动管理内存。
-
周期性:垃圾回收器会按照固定的时间间隔周期性地执行垃圾回收操作。
-
减少内存泄漏:通过自动释放不再使用的内存,垃圾回收机制有助于减少内存泄漏问题。
方式
前端垃圾回收机制在JavaScript等前端技术中扮演着至关重要的角色,它负责自动管理内存,防止内存泄漏。以下是关于前端垃圾回收机制中常用的算法和方式的详细讲解:
⚡️1. 标记-清除(Mark-Sweep)
标记清除是浏览器常见的垃圾回收方式
-
标记"可达"对象
当垃圾回收开始时,垃圾收集器会从一组被称为"根"的对象开始。
根对象:在JavaScript中,全局对象(在浏览器中是window对象)就是一个根对象。此外,调用栈中的变量也是根对象,因为它们直接指向了正在使用的对象。
垃圾收集器会从这些根对象开始,递归地访问它们的所有属性,并将访问到的对象都标记为
"可达"
或"活动"
对象。 这个过程会继续进行,直到所有可达对象都被标记。 -
标记"不再可达"对象
当变量不再被需要时,JavaScript引擎会将其标记为
"不再可达"
。 这通常发生在以下几种情况:- 局部变量在其作用域结束时离开作用域
- 全局变量被显式设置为null或undefined
- 对象的属性被删除或重新设置为其他值,导致原先的对象不再被引用
.
这意味着该变量所引用的值不再被任何活动的执行上下文所引用,它就被视为"垃圾"或"无用"的数据。
这些变量或对象所占用的内存空间会被垃圾回收机制标记为准备回收的状态。
-
清除没有被标记为"可达"的对象
在标记阶段完成后,垃圾收集器会进入清除阶段。
标记阶段完成后,垃圾收集器会进入清除阶段。在这个阶段,它会遍历堆中的所有对象,并找到那些没有被标记为
"可达"
的对象。这些对象就是所谓的"垃圾",因为它们不再被任何活动的执行上下文所引用。垃圾收集器会释放这些对象所占用的内存,以便将来可以重新使用。
.
2. 分代收集(Generational Collection)
分代收集将内存分为新生代和老生代,根据不同对象的特点采用不同的垃圾回收策略。
分代收集是V8等现代JavaScript引擎使用的策略之一,但开发者无需直接操作。
-
新生代
对象存活时间较短,如临时变量等。
采用复制算法(Copying)或Scavenge算法,将新生代内存划分为两个等大小的区域,每次只使用其中一个区域,当该区域内存不足时,将存活对象复制到另一个区域,然后清理当前区域。
-
老生代
对象存活时间较长,如全局变量等。
采用标记-清除或标记-整理(Mark-Compact)算法进行垃圾回收。标记-整理算法在标记阶段后,会将存活对象向一端移动,然后直接清理边界以外的内存,从而解决内存碎片问题。
3. 引用计数(Reference Counting)
注:JavaScript的主流引擎(如V8)并不使用引用计数算法进行垃圾回收
引用计数是一种简单的垃圾回收算法,它追踪每个值被引用的次数。
当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。
相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。
当一个值没有任何引用时,即引用计数为0时,说明这个变量已经没有价值,该值所占用的内存就会被释放。
-
优点:实现简单
-
缺点:不能解决循环引用问题
例如,对象A引用了对象B,同时对象B也引用了对象A,即使这两个对象都不再被其他变量引用,由于它们之间的循环引用,引用计数永远不会为0,导致内存无法释放。
js// 假设的引用计数示例(非实际JavaScript环境) let objA = { name: 'A' }; let objB = { name: 'B', refToA: objA }; objA.refToB = objB; // 此时objA和objB相互引用,引用计数均不为0
当没有其他引用指向objA和objB时,理论上它们应该被回收。但由于循环引用,引用计数算法无法正确回收它们
这种情况下,就要手动释放变量占用的内存:
jsobjA = null; // 假设这会减少objA的引用计数 objB = null; // 假设这会减少objB的引用计数 // 如果引擎使用引用计数,此时objA和objB的引用计数都为0,可以被回收
减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
-
重用对象延长生命周期:频繁地创建和销毁短期生命周期的对象会增加垃圾回收器的负担。尝试重用对象或将其生命周期延长,以减少不必要的内存分配和垃圾回收。
-
避免不必要的全局变量:全局变量的生命周期与应用程序的运行时间相同,因此它们会一直占用内存。如果可能的话,将变量限制在函数的作用域内,以便在函数执行完毕后自动回收它们所占用的内存。
-
解除不再需要的引用:当不再需要某个对象时,显式地将引用设置为null或将其从数组中删除,以便垃圾回收器能够更快地回收它们所占用的内存。
-
使用数据结构:选择合适的数据结构来存储数据,以减少不必要的内存占用和垃圾回收。例如,如果你知道将要存储的数据量很大,那么使用Array而不是Object可能会更合适,因为Array在内存中占用的空间更小。
-
避免内存泄漏:内存泄漏是指应用程序长时间占用内存,即使不再需要这些内存。内存泄漏可能会导致应用程序的性能下降,甚至崩溃。因此,要特别注意避免内存泄漏,例如及时清除事件监听器、取消不再需要的定时器、释放不再使用的DOM元素等。
内存泄漏
内存泄漏通常发生在JavaScript程序中,当对象在不再需要时仍然被引用,导致垃圾回收器无法回收其占用的内存时。以下是一些常见的导致内存泄漏的情况:
-
全局变量使用不当
在函数内部未使用var、let或const声明变量时,该变量会成为全局对象的属性,导致它无法被垃圾回收。
意外地创建全局变量,比如遗漏了var等关键字。
-
闭包使用不当
闭包可以保持对外部函数作用域的引用,如果闭包内部的引用一直存在,外部函数的作用域(包括其变量)也不会被垃圾回收。
-
定时器未清理
使用setTimeout或setInterval创建的定时器如果没有被清除(使用clearTimeout或clearInterval),它们会一直存在,即使不再需要。这可能导致相关的函数和数据持续占用内存。
-
DOM引用未清理
如果JavaScript代码中保留了对DOM元素的引用,而这些元素已经从DOM树中删除或替换,那么这些引用仍然会存在,导致内存泄漏。
同样,当元素被移除或替换时,与之相关的事件监听器也需要被显式移除,否则也会导致内存泄漏。
-
循环引用
当两个或多个对象相互引用,形成一个循环引用的链条,并且这些对象不再被外部引用时,它们就无法被垃圾回收。这通常发生在对象之间的互相引用,比如父子关系或闭包。
触发时机
垃圾回收的触发时机取决于JavaScript引擎的具体实现和当前的运行环境。
然而,通常来说,JavaScript引擎会在以下几种情况下开始执行垃圾回收:
-
内存分配阈值达到
当内存占用超过一个特定的阈值或达到某个预定的分配量时,垃圾回收器可能会被触发。这个阈值可以根据浏览器的实现和用户的配置而有所不同。
-
空闲时间
如果浏览器或JavaScript运行环境有空闲时间(即没有执行任何JavaScript代码或处理其他任务),垃圾回收器可能会选择在此时执行垃圾回收。这样可以最大化应用的响应时间,因为它不会打断正在执行的代码。
-
显式调用
虽然开发者通常不能直接控制垃圾回收的触发,但在某些环境中,可能会提供API来请求垃圾回收的执行。然而,这通常是不推荐的,因为开发者应该信任JavaScript引擎能够自动管理内存。
-
对象生命周期结束
在某些情况下,当JavaScript引擎可以明确地知道某个对象不再需要时(例如,当函数执行完毕,局部变量离开作用域时),它可能会立即触发垃圾回收来回收这些对象占用的内存。然而,这通常只是垃圾回收过程的一部分,而不是整个过程的触发条件。