关于javascript:前端面试常考题JS垃圾回收机制

摘要:家喻户晓,应用程序在运行过程中须要占用肯定的内存空间,且在运行过后就必须将不再用到的内存开释掉,否则就会呈现下图中内存的占用继续升高的状况,一方面会影响程序的运行速度,另一方面重大的话则会导致整个程序的解体。

家喻户晓,应用程序在运行过程中须要占用肯定的内存空间,且在运行过后就必须将不再用到的内存开释掉,否则就会呈现下图中内存的占用继续升高的状况,一方面会影响程序的运行速度,另一方面重大的话则会导致整个程序的解体。

JavaScript中的内存治理

  • 内存:由可读写单元组成,示意一片可操作空间;
  • 治理:人为的去操作一片空间的申请、应用和开释;
  • 内存治理:开发者被动申请空间、应用空间、开释空间;
  • 治理流程:申请-应用-开释

局部语言须要(例如C语言)须要手动去开释内存,然而会很麻烦,所以很多语言,例如JAVA都会提供主动的内存管理机制,称为“垃圾回收机制”,JavaScript语言中也提供了垃圾回收机制(Garbage Collecation),简称GC机制

全进展(Stop The World )

在介绍垃圾回收算法之前,咱们先理解一下「全进展」。垃圾回收算法在执行前,须要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑,这种行为称为 「全进展」(Stop The World)。例如,如果一次GC须要50ms,应用逻辑就会暂停50ms。

全进展的目标,是为了解决应用逻辑与垃圾回收器看到的状况不统一的问题。

举个例子,在自助餐厅吃饭,高高兴兴地取完食物回来时,后果发现自己餐具被服务员收走了。这里,服务员好比垃圾回收器,餐具就像是调配的对象,咱们就是应用逻辑。在咱们看来,只是将餐具长期放在桌上,然而服务员看来感觉你曾经不须要应用了,因而就收走了。你与服务员对于同一个事物看到的状况是不统一,导致服务员做了与咱们不冀望的事件。因而,为防止应用逻辑与垃圾回收器看到的状况不统一,垃圾回收算法在执行时,须要进行应用逻辑。

JavaScript中的垃圾回收

JavaScript中会被断定为垃圾的情景如下:

  • 对象不再被援用;
  • 对象不能从根上拜访到;

GC算法

常见的GC算法如下:

  • 援用计数
  • 标记革除
  • 标记整顿
  • 分代回收
援用计数

晚期的浏览器最常应用的垃圾回收办法叫做”援用计数“(reference counting):语言引擎有一张”援用表”,保留了内存外面所有的资源(通常是各种值)的援用次数。如果一个值的援用次数是0,就示意这个值不再用到了,因而能够将这块内存开释。

const user1 = {age: 11}
const user2 = {age: 22}
const user3 = {age: 33}

const userList = [user1.age, user2.age, user3.age]

下面这段代码,当执行过一遍过后,user1、user2、user3都是被userList援用的,所以它们的援用计数不为零,就不会被回收

function fn() {
    const num1 = 1
    const num2 = 2
}

fn()

下面代码中fn函数执行结束,num1、num2都是局部变量,执行过后,它们的援用计数就都为零,所有这样的代码就会被当做“垃圾”,进行回收。

援用计数算法有一个比拟大的问题: 循环援用

function objGroup(obj1, obj2) {
    obj1.next = obj2
    obj2.prev = obj1

    return {
        o1: obj1,
        o2: obj2,
    }
}

let obj = objGroup({name: 'obj1'}, {name: 'obj2'})
console.log(obj)

下面的这个例子中,obj1和obj2通过各自的属性互相援用,所有它们的援用计数都不为零,这样就不会被垃圾回收机制回收,造成内存节约。

援用计数算法其实还有一个比拟大的毛病,就是咱们须要独自拿出一片空间去保护每个变量的援用计数,这对于比拟大的程序,空间开销还是比拟大的。

援用计数算法长处:

  • 援用计数为零时,发现垃圾立刻回收;
  • 最大限度缩小程序暂停;

援用计数算法毛病:

  • 无奈回收循环援用的对象;
  • 空间开销比拟大;
标记革除(Mark-Sweep)

核心思想:分标记和革除两个阶段实现。

  1. 遍历所有对象找标记流动对象;
  2. 遍历所有对象革除没有标记对象;
  3. 回收相应的空间。

标记革除算法的长处是:比照援用计数算法,标记革除算法最大的长处是可能回收循环援用的对象,它也是v8引擎应用最多的算法。
标记革除算法的毛病是:

上图咱们能够看到,红色区域是一个根对象,就是一个全局变量,会被标记;而蓝色区域就是没有被标记的对象,会被回收机制回收。这时就会呈现一个问题,外表上蓝色区域被回收了三个空间,然而这三个空间是不间断的,当咱们有一个须要三个空间的对象,那么咱们刚刚被回收的空间是不能被调配的,这就是“空间碎片化”。

标记整顿(Mark-Compact)

为了解决内存碎片化的问题,进步对内存的利用,引入了标记整顿算法。

标记整顿能够看做是标记革除的加强。标记阶段的操作和标记革除统一。

革除阶段会先执行整顿,挪动对象地位,将存活的对象挪动到一边,而后再清理端边界外的内存。

标记整顿的毛病是:挪动对象地位,不会立刻回收对象,回收的效率比较慢。

增量标记(Incremental Marking)

为了缩小全进展的工夫,V8对标记进行了优化,将一次进展进行的标记过程,分成了很多小步。每执行完一小步就让应用逻辑执行一会儿,这样交替屡次后实现标记。

长时间的GC,会导致利用暂停和无响应,将会导致蹩脚的用户体验。从2011年起,v8就将「全暂停」标记换成了增量标记。改良后的标记形式,最大进展工夫缩小到原来的1/6。

v8引擎垃圾回收策略

  • 采纳分代回收的思维;
  • 内存分为新生代、老生代;

针对不同对象采纳不同算法:
(1)新生代:对象的存活工夫较短。新生对象或只通过一次垃圾回收的对象。
(2)老生代:对象存活工夫较长。经验过一次或屡次垃圾回收的对象。

V8堆的空间等于新生代空间加上老生代空间。且针对不同的操作系统对空间做了内存的限度。

针对浏览器来说,这样的内存是足够应用的。限度内存的起因:

针对浏览器的GC机制,通过一直的测试,如果内存再设置大一点,GC回收的工夫就会达到用户的感知,会造成感知上的卡顿。

回收新生代对象

回收新生代对象次要采纳复制算法Scavenge 算法)加标记整顿算法。而Scavenge 算法的具体实现,次要采纳了Cheney算法

Cheney算法将内存分为两个等大空间,应用空间为From,闲暇空间为To

查看From空间内的存活对象,若对象存活,查看对象是否合乎降职条件,若符合条件则降职到老生代,否则将对象从 From 空间复制到 To 空间。若对象不存活,则开释不存活对象的空间。实现复制后,将 From 空间与 To 空间进行角色翻转。

对象降职机制

一轮GC还存活的新生代须要降职。
当对象从From 空间复制到 To 空间时,若 To 空间应用超过 25%,则对象间接降职到老生代中。设置为25%的比例的起因是,当实现 Scavenge 回收后,To 空间将翻转成From 空间,持续进行对象内存的调配。若占比过大,将影响后续内存调配。

回收老生代对象

回收老生代对象次要采纳标记革除标记整顿增量标记算法,次要应用标记革除算法,只有在内存调配有余时,采纳标记整顿算法。

  1. 首先应用标记革除实现垃圾空间的回收
  2. 采纳标记整顿进行空间优化
  3. 采纳增量标记进行效率优化

新生代和老生代回收比照

新生代因为占用空间比拟少,采纳空间换工夫机制。
老生代区域空间比拟大,不太适宜大量的复制算法和标记整顿,所以最罕用的是标记革除算法,为了就是让全进展的工夫尽量减少。

内存透露识别方法

咱们先写一段比拟耗费内存的代码:

<button class="btn">点击</button>

<script>
    const btn = document.querySelector('.btn')
    const arrList = []

    btn.onclick = function() {
        for(let i = 0; i < 100000; i++) {
            const p = document.createElement('p')
            // p.innerHTML = '我是一个p元素'
            document.body.appendChild(p)
        }

        arrList.push(new Array(1000000).join('x'))
    }
</script>

应用浏览器的Performance来监控内存变动

点击录制,而后咱们操作们感觉耗费性能的操作,操作实现之后,点击stop进行录制。

而后咱们看一看是那些中央引起了内存的透露,咱们只须要关注内存即可。

能够看到内存在短时间耗费的比拟快,降落的小凹槽,就是浏览器在进行垃圾回收。

性能优化

1.防止应用全局变量

  • 全局变量会挂载在window下;
  • 全局变量至多有一个援用计数;
  • 全局变量存活更久,继续占用内存;
  • 在明确数据作用域的状况下,尽量应用局部变量;

2.缩小判断层级

function doSomething(part, chapter) {
    const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']

    if (part) {
        if (parts.includes(part)) {
            console.log('属于以后课程')
            if (chapter > 5) {
                console.log('您须要提供 VIP 身份')
            }
        }
    } else {
        console.log('请确认模块信息')
    }
}

doSomething('Vue', 6)

// 缩小判断层级
function doSomething(part, chapter) {
    const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']

    if (!part) {
        console.log('请确认模块信息')
        return
    }

    if (!parts.includes(part)) return
    console.log('属于以后课程')

    if (chapter > 5) {
        console.log('您须要提供 VIP 身份')
    }
}

doSomething('Vue', 6)

3.缩小数据读取次数
对于频繁应用的数据,咱们要对数据进行缓存。

<div id="skip" class="skip"></div>

<script>
    var oBox = document.getElementById('skip')

    // function hasEle (ele, cls) {
    //     return ele.className === cls
    // }

    function hasEle (ele, cls) {
        const className = ele.className
        return className === cls
    }

    console.log(hasEle(oBox, 'skip'))
</script>

4.缩小循环体中的流动

var test = () => {
    var i
    var arr = ['Hello World!', 25, '岂曰无衣,与子同袍']
    for(i = 0; i < arr.length; i++) {
        console.log(arr[i])
    }
}

// 优化后,将arr.length独自提出,避免每次循环都获取一次
var test = () => {
    var i
    var arr = ['Hello World!', 25, '岂曰无衣,与子同袍']
    var len = arr.length
    for(i = 0; i < len; i++) {
        console.log(arr[i])
    }
}

5.事件绑定优化

<ul class="ul">
    <li>Hello World!</li>
    <li>25</li>
    <li>岂曰无衣,与子同袍</li>
</ul>

<script>
    var list = document.querySelectorAll('li')
    function showTxt(ev) {
        console.log(ev.target.innerHTML)
    }

    for (item of list) {
        item.onclick = showTxt
    }

    // 优化后
    function showTxt(ev) {
        var target = ev.target
        if (target.nodeName.toLowerCase() === 'li') {
            console.log(ev.target.innerHTML)
        }
    }

    var ul = document.querySelector('.ul')
    ul.addEventListener('click', showTxt)
</script>

6.避开闭包陷阱

<button class="btn">点击</button>

<script>
    function foo() {
        let el = document.querySelector('.btn')
        el.onclick = function() {
            console.log(el.className)
        }
    }
    foo()

    // 优化后
    function foo1() {
        let el = document.querySelector('.btn')
        el.onclick = function() {
            console.log(el.className)
        }
        el = null // 将el置为 null 避免闭包中的援用使得不能被回收
    }
    foo1()
</script>

本文分享自华为云社区《Vue进阶(幺陆玖):JS垃圾回收机制》,原文作者:SHQ5785 。

点击关注,第一工夫理解华为云陈腐技术~

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理