一文详解Javascript内存机制与垃圾回收

作者:半夏时间:2026-10-30 20:22:03浏览:622

本文深入解析JavaScript的内存管理机制,区分原始类型与引用类型的存储差异,并详细阐述V8引擎的分代收集策略。内容涵盖新生代Scavenge算法与老生代Mark-Sweep/Mark-Compact算法原理,以及通过增量标记解决全停顿问题的技术方案,帮助开发者优化内存使用与性能。

一 内存机制

1.1 数据类型

JavaScript 属于动态类型语言,即在运行时检查数据类型,同时也属于弱类型语言,允许同一变量存储不同类型的数据。

JavaScript 共包含 8 种数据类型:Boolean、Null、Undefined、Number、BigInt、String、Symbol 以及 Object。前 7 种数据类型为原始类型,而Object则被归类为引用类型。这两类数据在内存中的存储位置存在显著差异。

1.2 内存空间

在 JavaScript 执行过程中,主要涉及三种类型的内存空间:代码空间、栈空间和堆空间。

代码空间主要用于存储可执行的代码指令。

原始类型的数据存储在栈空间中,而引用类型的数据则存储在堆空间中。

具体说明示例如下:

function foo(){
    var a = "极客时间"
    var b = a
    var c = {name:"极客时间"}
    var d = c
}
foo()

栈空间,即前文提到的调用栈,用于存储执行上下文。

当上述代码执行至第 3 行时,变量 a 和 b 的值直接保存在执行上下文中。由于执行上下文被压入栈空间,因此可以认为变量 a 和 b 均存放在栈空间中。

一 内存机制

执行到第 4 行时,JavaScript 引擎判断变量 c 的值为引用类型,于是将该值分配至堆空间。分配后,该值在堆空间中拥有一个地址,该地址被赋值给变量 c。在第 5 行,将 c 赋值给 d,实际上是将引用地址赋给了 d。因此,修改引用类型对象的值,实质是修改堆空间中数据的值。

一 内存机制

栈空间因需维护执行上下文,影响程序执行效率,故空间通常较小,适合存放原始类型的小数据;引用类型数据可能较大,因此堆空间较大。不过,堆空间在分配和回收内存时会占用一定时间。

栈空间切换执行上下文状态的过程如下:

一 内存机制

1.3 闭包内的数据存储

闭包内的变量是存储在栈空间还是堆空间?

具体说明示例如下:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

执行上述代码,当 foo 函数执行完毕后,调用栈中的 foo 函数执行上下文会被销毁。由于变量 myName 和 test1 持续存在外部引用,JavaScript 引擎判断这是一个闭包,会在堆空间中创建一个closure(foo)的对象,该对象包含这两个被引用的变量。

一 内存机制

二 垃圾回收

部分数据在使用后不再需要,若继续保留在内存中并逐渐积累,将占用越来越多的空间。通过垃圾回收机制可以释放有限的内存空间。

2.1 不同语言的垃圾回收策略

一般而言,垃圾回收分为手动回收和自动回收两种策略。

以 C / C++ 为例,采用的是手动回收策略,内存的分配与销毁均由程序员编写的代码控制。若不主动销毁垃圾数据,将导致内存泄漏。

以 Javascript / Java 为例,垃圾数据由内置的垃圾回收器自动回收,无需通过编写代码手动释放。

2.2 调用栈的垃圾回收机制

JavaScript 引擎在将执行上下文压入调用栈的同时,使用一个记录当前执行状态的指针(ESP)指向当前的执行上下文,表示正在执行该执行上下文。

当该执行上下文运行完毕,ESP 下移,这个下移的操作即为销毁被执行过的上下文的过程。

示例说明如下:

function foo(){
    var a = 1
    var b = {name:"极客邦"}
    function showName(){
      var c = 2
      var d = {name:"极客时间"}
    }
    showName()
}
foo()

一 内存机制

2.3 堆空间垃圾回收机制

2.3.1 代际假说

代际假说观点认为,大部分对象在内存中有用的时间很短,一经分配内存,很快就变得不可访问;而少量数据持续活跃,存活时间较长。

2.3.2 V8 垃圾回收机制:分代收集

基于代际假说,V8 把堆分为老生代和新生代,新生代中存放的是生存时间短的对象(1—8M空间),老生代中存放的是生存时间久的对象(空间比较大)。

针对新生代和老生代,V8 使用不同的回收机制,以便更高效地进行垃圾回收。使用主垃圾回收器回收老生代的垃圾数据,使用副垃圾回收器回收新生代的垃圾数据。

垃圾回收原理

  • 标记空间中的活动对象(还在使用的对象)和非活动对象(可以回收的对象)。
  • 标记完成之后,统一清理内存中所有被标记的可回收对象,回收这部分对象占据的内存。
  • 内存整理:频繁回收对象之后,内存中存在大量不连续空间(内存碎片),如果要分配较大连续内存时,可能出现内存不足的情况,所以需要将内存碎片整理成连续的空间。

副垃圾回收器

主要负责新生代的垃圾回收。新生代空间较小,里面的对象一般较小;回收频繁。

回收机制采用 Scavenge 算法。该算法把新生代空间划分为对象区域和空闲区域。

一 内存机制

新加入的对象存到对象区域,当对象区域快被写满时,执行一次垃圾回收。在回收过程中,对对象区域里的对象做标记,标记完成后,把存活的对象复制到空闲区域中并有序地排列起来(消除内存碎片),把无用的对象清理掉。

复制完成后,对象区域和空闲区域进行角色翻转,对象区域变成空闲区域,空闲区域变成对象区域。这样这两块区域可以无限重复使用下去。

复制操作需要时间成本,因此为了执行效率,新生区的空间一般设置的比较小。

经过两次垃圾回收依然存活的对象,会被移动到老生区中,以防止过多的存活对象挤满新生区。

主垃圾回收器

主要负责老生区的垃圾回收。老生区的里的对象一般占用空间大(因复制操作耗时所以不适合使用 Scavenge 算法),存活的时间比较长。

回收机制采用的是 标记-清除(Mark-Sweep)算法。从一组根元素开始,递归遍历这组根元素,在遍历过程中能到达的元素称为活动对象,没有达到的元素判断标记为垃圾数据。

一 内存机制

标记完成之后,执行清除过程。

一 内存机制

如上图所示,清除后会产生大量不连续的内存碎片;为了避免内存碎片,进化出了另一种算法:标记-整理(Mark-Compact),标记完成后,让所有存活的对象移向内存一端,然后直接清理掉端边界外的数据。

全停顿

JavaScript 是运行在主线程上的(单线程),一旦执行垃圾回收算法,其他 JavaScript 脚本将被阻塞,需要等待垃圾回收完毕才能继续执行。这种行为叫做全停顿。

一 内存机制

如上图所示,过长的全停顿将导致页面卡顿。新生代因其空间小、存活对象占用空间小,对全停顿影响不大,老生代的垃圾回收是造成全停顿的主因。为了降低老生代垃圾回收造成的卡顿,V8 将标记过程分为一个个子标记,让子标记和 JavaScript 脚本交替进行,这个解决方案叫做增量标记算法。

一 内存机制

以上就是一文详解Javascript内存机制与垃圾回收的详细内容,更多关于Javascript内存机制与垃圾回收的资料请关注本站其它相关文章!

作者最新文章