关于浏览器:V8引擎的JavaScript内存机制

55次阅读

共计 3947 个字符,预计需要花费 10 分钟才能阅读完成。

对于前端攻城师来说,JS 的内存机制不容忽视。如果想成为行业专家,或者打造高性能前端利用,那就必须要弄清楚 JavaScript 的内存机制了

先看栗子

    function foo (){
        let a = 1
        let b = a
        a = 2
        console.log(a) // 2
        console.log(b) // 1
        
        let c = {name: '掘金'}
        let d = c
        c.name = '沐华'
        console.log(c) // {name: '沐华'}
        console.log(d) // {name: '沐华'}
    }
    foo()

能够看出在咱们批改不同数据类型的值后,后果有点不一样。

这是因为不同数据类型在内存中存储的地位不一样,在 JS 执行过程中,次要有三种内存空间:代码空间

代码空间次要就是存储可执行代码,对于这个内容有点多,能够看我另一篇文章有具体介绍

咱们先看一下栈和堆

栈和堆

在 JS 中,每一个数据都须要一个内存空间。而不同的内存空间有什么区别特点呢?,如图

调用栈也叫执行栈,它的执行准则是先进后出,后执行的会先出栈,如图


栈:

  • 存储根底类型Number, String, Boolean, null, undefined, Symbol, BigInt
  • 存储和应用形式 后进先出(就像一个瓶子, 后放进去的货色先拿进去)
  • 主动分配内存空间,主动开释,占固定大小的空间
  • 存储援用类型的变量,但实际上保留的不是变量自身,而是指向该对象的 指针(在堆内存中寄存的地址)
  • 所有办法中定义的变量存在栈中,办法执行完结,这个办法的内存栈也主动销毁
  • 能够递归调用办法,这样随着栈深度减少,JVW 维持一条长长的办法调用轨迹,内存不够调配,会产生栈溢出

堆:

  • 存储援用类型Object(Function/Array/Date/RegExp)
  • 动态分配内存空间,大小不定也不会主动开释
  • 堆内存中的对象不会因为办法执行完结就销毁,因为有可能被另一个变量援用(参数传递等)

为什么会有栈和堆之分

通常与 垃圾回收机制 无关。每一个办法执行时都会建设本人的内存栈,而后将办法里的变量一一放入这个内存栈中,随着办法执行完结,这个办法的内存栈也会主动销毁

为了使程序运行时占用的内存最小,栈空间都不会设置太大,而堆空间则很大

每创立一个对象时,这个对象会被保留到堆中,以便重复复用,即便办法执行完结,也不会销毁这个对象,因为有可能被另一个变量 (参数传递等) 援用,直到对象没有任何援用时才会被零碎的垃圾回收机制销毁

而且 JS 引擎 须要用栈来维护程序执行期间上下文的状态,如果所有的数据都在栈里在,栈空间大了的话,会影响到上下文切换的效率,进而影响整个程序的执行效率

内存泄露和垃圾回收

下面说了在 JS 中创立变量(对象,字符串等)时都分配内存,并且在不再应用它们时“主动”开释内存,这个主动开释内存的过程称为 垃圾回收。也正是因为垃圾回收机制的存在,让很多开发者在开发中不太关怀内存治理,所在在一些状况下导致内存泄露

内存生命周期:

  1. 内存调配:当咱们申明变量,函数,对象的时候,零碎会主动为它们分配内存
  2. 内存应用:即读写内存,也就是应用变量,函数,参数等
  3. 内存回收:应用结束,由垃圾回收机制主动回收不再应用的内存

局部变量(函数外部的变量),当函数执行完结,没有其余援用(闭包),该变量就会被回收

全局变量的生命周期直到浏览器卸载页面才会完结,也就是说 全局变量不会被垃圾回收

内存泄露

程序的运行须要内存,对于继续运行的服务过程,必须及时开释不再用到的内存,否则内存占用越来越大,轻则影响零碎性能,重大的会导致过程解体

内存泄露就是因为忽略或者谬误,导致程序不能及时开释那些不再应用的内存,造成内存的节约

判断内存泄露

Chrome 浏览器 中,能够这样查看内存占用状况

开发者工具 => Performance => 勾选 Memory => 点左上角 Record => 页面操作后点 stop

而后就会显示这段时间内的内存应用状况了

  1. 一次查看内存占用状况后,看以后内存占用趋势图,走势呈上升趋势,能够认为存在内存泄露
  2. 屡次查看内存占用状况后截图比照,比拟每次内存占用状况,如果呈上升趋势,也能够认为存在内存泄露

Node 中,应用 process.memoryUsage 办法查看内存状况

console.log(process.memoryUsage());
  • heapUsed:用到的堆的局部。
  • rss(resident set size):所有内存占用,包含指令区和堆栈。
  • heapTotal:” 堆 ” 占用的内存,包含用到的和没用到的。
  • external:V8 引擎外部的 C++ 对象占用的内存

判断内存泄露以 heapUsed 字段为准

什么状况下会造成内存泄露

  1. 没有申明而意外创立的全局变量
  2. 被忘记的定时器和回调函数,没有及时敞开定时器中的援用会始终留在内存中
  3. 闭包
  4. DOM 操作援用(比方援用了 td 却删了整个 table, 内存会保留整个 table)

内存泄露如何防止

所以记住一个准则:不必的货色,及时偿还,有道是,有借有还,再借不难

  1. 缩小不必要的全局变量,比方应用严格模式防止创立意外的全局变量
  2. 缩小生命周期较长的对象,防止过多的对象
  3. 应用完数据后,及时解除援用(闭包中的变量,DOM 援用, 定时器革除)
  4. 组织好逻辑,防止死循环造成浏览器卡顿,解体

垃圾回收

JS 是有主动垃圾回收机制的,那么这个主动垃圾收集机制是怎么工作的呢?

回收执行栈中数据

看栗子

function foo(){
    let a = 1
    let b = {name: '沐华'}
    function showName(){
        let c = 2
        let d = {name: '沐华'}
    }
    showName()}
foo()

执行过程:

  • JS 引擎先为 foo 函数创立执行上下文,并将执行上下文压入执行栈
  • 执行遇到 showName 函数,再为 showName 函数创立执行上下文,并将执行上下文压入执行栈中,所以在栈中 showName 压在 foo 的下面
  • 而后先执行 showName 函数的执行上下文,JS 引擎中有一个记录以后执行状态的指针(ESP),会指向正在执行的上下文,也就是 showName
  • 当 showName 执行完结之后,执行流程就进入下一个执行上下文,也就是 foo 函数,这时就须要销毁 showName 执行上下文了。次要就是 JS 引擎将 ESP 指针下移,指向 showName 上面的执行上下文,也就是 foo,这个下移操作就是销毁 showName 函数执行上下文的过程

如图

回收堆中的数据

其实就是找出那些不再持续应用的值,而后开释其占用的内存。

比方方才的栗子,当 foo 函数和 showName 函数执行上下文都执行完结就清理了,然而外面的两个对象还仍然占用着空间,因为对象的数据是存在堆中的,清理掉的栈中的只是对象的援用地址,并不是对象数据

这就须要 垃圾回收器

垃圾回收阶段最艰巨的工作就是找到不须要的变量,所以垃圾回收算法有很多种,并没有哪一种能胜任所有场景,须要依据场景衡量抉择

援用计数

援用计数是以前的垃圾回收算法,该算法定义 ” 内存不再应用 ” 的规范很简略,就是看一个对象是否有指向它的援用,如果没有其余对象指向它,就阐明该对象不再须要了

但它却有一个致命的问题:循环援用

就是如果有两个对象相互援用,只管他们已不再应用,然而垃圾回收不会进行回收,导致内存泄露

为了解决循环援用造成的问题,古代浏览器都没有采纳援用计数的形式

在 V8 中会把堆分为新生代和老生代两个区域

新生代和老生代

V8 实现了 GC 算法,采纳了分代式垃圾回收机制,所以 V8 将堆内存分为 新生代 (副垃圾回收器) 和老生代 (主垃圾回收器) 两个局部

新生代

新生代中通常只反对 1~8M 的容量,所以次要 寄存生存工夫较短的对象

新生代中应用 Scavenge GC 算法,将新生代空间分为两个区域:对象区域和闲暇区域。如图:

顾名思义,就是说这两块空间只应用一个,另一个是闲暇的。工作流程是这样的

  • 将新调配的对象存入对象区域中,当对象区域存满了,就会启动 GC 算法
  • 对对象区域内的垃圾做标记,标记实现之后将对象区域中还存活的对象复制到闲暇区域中,曾经不必的对象就销毁。这个过程不会留下内存碎片
  • 复制实现后,再将对象区域和闲暇调换。既回收了垃圾也能让新生代中这两块区域有限重复使用上来

正因为新生代中空间不大,所以就容易呈现被塞满的状况,所以

  • 经验过两次垃圾回收仍然还存活的对象会被移到老生代空间中
  • 如果闲暇空间对象的占比超过 25%,为了不影响内存调配,就会将对象转移到老生代空间

老生代

老生代特点就是 占用空间大 ,所以次要 寄存存活工夫长的对象

老生代中应用 标记革除算法 标记压缩算法。因为如果也采纳 Scavenge GC 算法的话,复制大对象就比拟花工夫了

标记革除

在以下状况下会先启动标记革除算法:

  • 某一个空间没有分块的时候
  • 对象太多超过空间容量肯定限度的时候
  • 空间不能保障新生代中的对象转移到老生代中的时候

标记革除的流程是这样的

  • 从根部 (js 的全局对象) 登程,遍历堆中所有对象,而后标记存活的对象
  • 标记实现后,销毁没有被标记的对象

因为垃圾回收阶段,会暂停 JS 脚本执行,等垃圾回收结束后再复原 JS 执行,这种行为称为 全进展(stop-the-world)

比方堆中数据超过 1G,那一次残缺的垃圾回收可能须要 1 秒以上,这期间是会暂停 JS 线程执行的,这就导致页面性能和响应能力降落

增量标记

所以在 2011 年,V8 从 stop-the-world 标记切换到 增量标记。应用增量标记算法,GC 能够将回收工作分解成很多小工作,穿插在 JS 工作两头执行,这样防止了利用呈现卡顿的状况

并发标记

而后在 2018 年,GC 技术又有重大突破,就是 并发标记 让 GC 扫描和标记对象时,容许 JS 同时运行

标记压缩

革除后会造成堆内存呈现内存碎片的状况,当碎片超过肯定限度后会启动 标记压缩算法,将存活的对象向堆中的一端挪动,到所有对象挪动实现,就清理掉不须要的内存

结语

点赞反对、手留余香、与有荣焉

参考

浏览器工作原理与实际

正文完
 0