目录
  • 一 内存机制
    • 1.1 数据类型
    • 1.2 内存空间
    • 1.3 闭包内的数据存储
  • 二 垃圾回收
    • 2.1 不同语言的垃圾回收策略
    • 2.2 调用栈的垃圾回收机制
    • 2.3 堆空间垃圾回收机制
      • 2.3.1 代际假说
      • 2.3.2 V8 垃圾回收机制:分代收集

一 内存机制

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 的值是引用类型,Javascript 引擎将该值分配到堆空间里,分配后该值会有一个在堆空间的地址,然后将该地址赋值给变量 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内存机制与垃圾回收的资料请关注本网站其它相关文章!

您可能感兴趣的文章:

  • JavaScript面试必备之垃圾回收机制和内存泄漏详解
  • JS堆栈内存的运行机制详解
  • JavaScript的垃圾回收机制与内存管理
  • 跟我学习javascript的垃圾回收机制与内存管理
  • 详解JavaScript的垃圾回收机制
  • 深入理解 JS 垃圾回收