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

36次阅读

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

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

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

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。

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

正文完
 0