美文网首页
【图解篇】前端内存管理

【图解篇】前端内存管理

作者: T_man | 来源:发表于2021-03-11 09:47 被阅读0次

    前端为什么要关注内存

    • 防止占用内存过大,造成页面卡顿,甚至无响应
    • Node.js 使用 V8 引擎,内存管理对于服务端至关重要,因为服务端的持久性,内存更容易积累造成内存溢出

    js 垃圾回收机制

    js 使用垃圾回收机制自动管理内存,这种方式的利弊都很明显。

    • 优势: 可以大幅简化程序中都内存管理代码,减轻开发者的负担,同时也减少长时间运转造成的内存泄漏问题
    • 劣势: 意味着开发者无法掌控内存管理,我们无法强迫其进行垃圾回收,进行管理

    下面简单介绍一下 js 的几种垃圾回收策略:

    引用计数

    主要是IE8 以下的浏览器使用,现代浏览器都弃用了这种方式,这里只做简单介绍。

    基本原理就是,记录跟踪每个值被引用的次数,被引用一次被引用次数就加一,被释放就减一,为零时,就释放改值所占内存。

    标记清除

    主流浏览器使用垃圾回收机制。

    当变量进入环境(例如,在函 数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。

    而当变量离开环境时,则将其 标记为“离开环境”。 可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境, 或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。

    function test(){
        var a = 10;    //被标记"进入环境"
        var b = "hello";    //被标记"进入环境"
    }
    test(); // 执行完毕后之后,a和b又被标记"离开环境",被回收
    

    说到底,如何标记变量其实并不重要,关键在于采取什么策略。 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。

    然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记 的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。

    最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

    环境可以理解为我们的作用域,但是全局作用域的变量只会在页面关闭才会销毁。

    V8 内存控制

    V8 的内存限制

    在 Node 中只能使用部分内存,64位系统下约为1.4GB,32位系统下约为0.7GB,在这种限制下,将会导致 Node 无法操作大内存对象,比如将一个 2GB 的文件读入内存,即使物理内存有64 GB,也没有办法完成,这个时候我们可以使用 Buffer 类,来完成大内存文件的读取。

    造成这个问题的主要原因: Node 基于 V8 构建,而 V8 的这套内存管理机制主要是在浏览器中使用,完全可以满足前端页面中的所有需求,但是在 Node 中却限制了开发者随心所欲使用大内存的想法。

    V8 的垃圾回收机制

    V8 的垃圾回收策略主要基于分代式垃圾回收机制。主要将内存分为新生代和老生代两代。

    图片

    新生代空间(Young Generaion)

    特点:

    • 管理对象存活时间较短
    • 占用空间比老生代空间小很多
    • 垃圾回特别频繁

    新生代空间的垃圾回收采用Scavenge 算法,其工作原理如下

    1. 将新生代空间分为两个空间,称为semispace,处于使用状态的叫做 From 空间,处于闲置的叫 To 空间,当我们分配对象时,先是在 From 空间中进行分配。
    图片
    1. 开始垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,然后释放 From 空间中的内存。
    图片 图片
    1. From 空间与 To 空间对换
    图片

    从上面的过程我们可以看到,Scavenge 算法是典型的牺牲空间换取时间的算法。缺点是只能使用堆内存中的一半,优点是在时间效率上有优异的表现。

    老生代空间( OldGeneraion)

    在新生代空间中生命周期较长的对象会被复制到老生代空间中,这个过程叫晋升。对象晋升的条件主要有两个:

    1. 对象是否经历过一次 Scavenge 回收。 对象从 From 空间复制到 To 空间时,会检查它的内存地址来判断这个对象是否已经经历过一次 Scavenge 回收。如果经历过,就直接复制到老生代空间中,而不是 To 空间。
    2. To 空间的内存使用占比是否超过 To 空间的 25%。 对象从 From 空间复制到 To 空间时,发现 To 空间的内存占比已经超过限制。因为To 空间将会变成 From空间,为了不影响后续的内存分配,会直接晋升到老生代空间中。

    对于老生代空间,由于存活对象占比较大,再采用Scavenge的方式会有两个问题:

    1. 存活对象比较多,复制存活对象的效率会很低
    2. 要拆分两个 semispace 空间,比较浪费

    为此,老生代中主要采用标记清除(Mark-Sweep)标记整理(Mark-Compact)相结合的方式进行垃圾回收,其工作原理如下

    • 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。标记后的示意图如下:
    图片

    黄色部分为标记活跃的对象,深灰色部分为标记死亡的对象。

    标记清除(Mark-Sweep)最大的问题在于,清除标记死亡的对象后,内存不连续,这种碎片空间会对后续的内存分配造成问题。

    • 为了解决内存碎片的问题,标记整理(Mark-Compact)被提出来。它会在标记完成后,将存活的对象移动到一端,然后释放存活对象这一端之外的空间。
    图片 图片

    增量标记(Incremental Marking)

    在进行上面 V8垃圾回收操作的时候,需要将应用逻辑暂停,但是由于老生代空间很大,且存活对象很多,为了避免长时间的停顿,将原本一次性完成的操作改为增量标记,即拆分为许多小“步进”,没做完一次“步进”,让应用逻辑执行一会儿,交替执行,直到垃圾回收执行完成。

    参考文章:

    1. 《Javascript 高级程序设计》
    2. 《深入浅出 Node.js》

    相关文章

      网友评论

          本文标题:【图解篇】前端内存管理

          本文链接:https://www.haomeiwen.com/subject/xusnqltx.html