在数据使用之后,并且不在被需要的数据称为垃圾数据,对这些垃圾数据需要进行回收来释放有限的内存空间。
垃圾回收有分为手动回收 和自动回收两种策略。
在Javascript中产生的垃圾数据是由垃圾回收器来回收的。
一、调用栈中的数据是如何回收的
在 JavaScript 中,垃圾回收是由浏览器的 JavaScript 引擎负责的。这个引擎会周期性地扫描内存,查找不再被引用的对象,然后释放这些对象占用的内存。当一个函数执行完毕,它的执行上下文就会被标记为不再需要,这时候就可能触发垃圾回收。
js
function foo() {
var a = { name: '竹合'};
function bar() {
var b = { name: '竹合'};
}
bar();
}
foo();
在代码执行的时候还有一个记录当前状态的指针称为ESP
。
当执行到bar()
函数的时候,ESP
就指向了这个函数,当执行完之后,函数执行流程就到了foo函数,也就需要销毁bar函数
的执行上下文,JS就会将ESP
下移到foo函数
的执行上下文,这个下移过程就是销毁bar函数
的执行上下文的过程。
二、堆中的数据是如何回收的
垃圾回收机制主要有两种策略:
1. 标记清除(Mark and Sweep):
这是 JavaScript 最常用的垃圾回收策略之一。它的基本原理是在执行上下文中标记那些仍然被引用的变量,然后清除未被标记的变量。这通常是通过追踪从全局对象开始的引用链来完成的。
2. 引用计数:
这种策略会为每个值都维护一个引用计数,当这个值的引用计数变为零时,说明它不再被引用,可以被回收。然而,引用计数策略容易出现循环引用的问题,因为如果两个对象互相引用,它们的引用计数永远不会变为零。
JavaScript 引擎中的垃圾回收器会在适当的时候自动运行。具体的触发时机和实现方式可能因浏览器引擎而异。一般来说,垃圾回收器会在空闲时执行,确保不会对页面性能产生明显的影响。
大部分现代浏览器主要采用标记-清除(Mark and Sweep) 作为其主要的垃圾回收策略。
标记清除的基本原理是通过标记那些仍然被引用的变量,然后清除未被标记的变量。垃圾回收器会从全局对象开始,通过引用链遍历对象,标记那些仍然被引用的对象,然后清除那些未被标记的对象。这个过程确保了内存中只保留了仍然可达的对象,不再被引用的对象就可以被安全地回收。
JavaScript 引擎中的具体垃圾回收实现可能会有一些优化和改进,以提高性能和减少对页面的影响。例如,V8 引擎(Chrome 使用的 JavaScript 引擎)使用了一种称为增量标记的技术,以减小垃圾回收的停顿时间。其他引擎也可能采用类似的优化策略。
V8 引擎中增量标记的基本工作流程(主要用在老生代中):
1. 初始标记阶段(Initial Marking):
- 在这个阶段,V8 首先执行一次快速的初始标记,标记那些直接与根对象关联的对象。这个阶段的目标是尽快完成,以减小初始的停顿时间。
2. 并发标记阶段(Concurrent Marking):
- 在并发标记阶段,V8 引擎启动后台线程,该线程负责继续标记其余的对象,但不会阻塞主线程执行 JavaScript 代码。这样,应用程序可以在并发标记的同时继续运行,减小了对用户体验的影响。
3. 重标记阶段(Re-Marking):
- 在并发标记完成后,V8 需要执行最后的重标记步骤,确保在并发标记期间发生变化的对象也被正确标记。这一步会导致短暂的停顿。
4. 清理阶段(Clean-Up):
- 清理阶段用于清理不再被引用的对象,释放它们占用的内存。这一阶段通常也是并发执行的。
回收堆中的垃圾数据就需要用到JS中的垃圾回收器。
代际假说(Generational Hypothesis) 是垃圾回收领域的一种理论,它提出了一个观点:大多数对象在内存中存在的时间很短,而只有一小部分对象会长时间存活。基于这个观点,代际假说提出了一种优化垃圾回收的方法,即将对象分为不同的代际,并根据它们的存活时间采用不同的回收策略。
代际假说的核心思想包括以下两点:
1. 新生代(Young Generation):
大多数对象在被创建后很短时间内就会被销毁,而只有一小部分对象会存活更长的时间。因此,将对象划分为新生代和老生代两个部分,新生代包含大部分短时间存活的对象。
2. 分代回收策略:
针对不同代际的对象采用不同的回收策略。新生代的对象使用一种较为频繁但简单的回收算法,例如复制算法(Copying Algorithm) ,以迅速清除短寿命的对象。而老生代的对象使用更复杂的回收算法,例如标记-清除(Mark and Sweep)算法,以处理存活时间较长的对象。
代际假说的优势在于,通过将对象分为新生代 和老生代,并使用不同的回收策略,可以更有效地提高垃圾回收的性能。大部分对象很快就会被清理,只有一小部分对象需要经历更复杂的回收过程。这使得垃圾回收可以更快地完成,减小了对应用性能的影响。
在V8引擎中会把堆分为新生代和老生代两个区域。新生代用来存放生存时间短的对象(通常支持1~8M的容量),老生代用来存放生存时间久的对象。
V8引擎也分别使用两个不同的垃圾回收器:
-
副垃圾回收器,负责回收新生代的垃圾。
-
主垃圾回收器,负责回收老生代的垃圾。
1. 垃圾回收器的工作流程
两种类型的垃圾回收器共用一套共同的流程。
-
首先是标记空间中的活动对象(还在使用的对象)和非活动对象(可以进行垃圾回收的对象)。
-
然后回收非活动对象所占据的内存。在所有标记完成之后,统一清理内存中的所有被标记可以回收的对象。
-
最后是内存整理:频繁回收对象之后,内存中会存在大量不连续空间(内存碎片)。当内存中出现了大量碎片,如果需要分配大量连续内存,就有可能出现内存空间不足的情况。
2. 副垃圾回收器
新生代用Scavenge算法
来处理,也就是将新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
新加入的对象都会存放到对象区域,当对象区域快被写满的时候就会执行一次垃圾清理操作。
在垃圾回收过程中,首先要对对象区域中的垃圾做标记,标记完成之后进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时还会把这些对象有序的排列起来,也就完成了内存整理的操作,完成复制之后空闲区域和对象区域进行翻转,这样就完成了垃圾对象的回收操作。
V8引擎采用了对象晋升策略,经过两次垃圾回收还存活的对象会被移动到老生区中。
3. 主垃圾回收器
主垃圾回收器存放的对象的特点是:1.对象占用空间大;2.对象存活时间长。
也就是采用复制对象的策略会会花费比较多的时间,所以主垃圾回收器采用的策略是标记-清除(Mark-Sweep) 和标记-整理(Mark-Compact) 两个算法进行垃圾回收的。
1. 标记-清除算法(Mark-Sweep):
a. 标记阶段:
- 初始标记(Initial Marking):
垃圾回收器从根对象开始,标记所有能够直接或间接访问到的对象。这个阶段的目标是快速标记那些与根对象直接关联的对象。
- 并发标记(Concurrent Marking):
在初始标记完成后,V8 引擎启动后台线程进行并发标记。这个阶段并发地标记那些与根对象间接关联的对象,不阻塞主线程执行 JavaScript 代码。
- 重标记(Re-Marking):
在并发标记完成后,需要执行最后的重标记步骤,确保在并发标记期间发生变化的对象也被正确标记。这一步会导致短暂的停顿。
b. 清除阶段:
- 清除无标记对象(Clear Unmarked)
2. 标记-整理算法(Mark-Compact):
在标记-清除算法的基础上,标记-整理算法引入了整理阶段,以减小内存碎片。
a. 整理阶段:
- 整理(Compacting Live Objects):
3. 增量标记(Incremental Marking):
V8 引擎的老生代垃圾回收器还包括增量标记技术。增量标记将标记阶段分解成多个小步骤,允许在标记过程中与 JavaScript 应用交替执行。这降低了垃圾回收导致的停顿时间,提高了应用的响应性。