这里有垃圾可以回收

248 阅读5分钟

前言

平时开发(复制粘贴)过程中,我们其实很少会考虑到内存的问题,毕竟我们不需要像 c 语言那样手动申请内存再释放内存,内存的申请和回收都由引擎帮我们搞定了。但是如果想写出更优秀的代码,清楚js的垃圾回收机制却是很重要的。

内存策略

js 中的内存分为栈内存和堆内存。

在讨论栈内存和堆内存之前,先要强调数据结构中的堆栈和内存堆栈是两回事,只是名字相似而已。

栈(数据结构):后进先出的一种数据结构

堆(数据结构):是一种完全二叉树

栈内存

由操作系统自动分配和回收的内存空间

【谁申请内存】:大小比较固定的值,所以基本类型的值(闭包除外)和引用类型的的地址会存在栈内存中。

【内存申请】:函数执行时,系统自动分配栈内存依序存储基本类型或者引用类型的引用。

【内存回收】:函数执行完毕,系统则自动依序清掉当前函数占用的内存

由于整个流程是,存入当前函数的所有变量,再清除当前函数所有的变量回到上一个函数,就像是数据结构中栈的后进先出,所以叫栈内存(鄙人拙见)。

堆内存

可以向操作系统申请的内存空间

此处需要注意的是,堆内存和数据结构中的堆是两回事,不要混为一谈。

【谁申请堆内存】:

  • 大小不固定(可以动态改变)的值,所以对象会申请
  • 闭包产生的变量

【内存申请】:操作系统有一个存储空闲地址的链表,链表由低地址向高地址遍历。当程序向操作系统申请时,遍历链表找到第一个空间大于申请空间的堆结点,将该结点从链表中删除,通常堆结点的大小和申请空间的大小并不是完全相同的,所以会将剩余空间重新插入链表。

【内存回收】:堆内存不会被系统自动回收,需要通过垃圾回收机制进行清除。

内存泄漏

不再用到的内存没有及时释放,就叫内存泄漏

垃圾回收

引用计数

引用计数是早期 ie 的垃圾回收策略,现在的浏览器基本都不采用该方式了。

机制大体流程:生成一个对象并将引用并存入变量,此时该对象的引用+1,如果变量的值赋成其它值,那么对象的引用-1,如果最后的引用为0,则会被垃圾回收。

但是引用计数存在循环引用的问题,致使不再被需要的变量将一直存在于内存中得不到释放。下面看栗子1:

function foo () {
    let obj1 = {}
    let obj2 = {}
    
    obj1.other = obj2
    obj2.other = obj1
}

函数执行完之后,obj1和obj2 变量已经被系统自动清除,即不再引用原来的对象,然而在堆内存内,原来的对象已经互相引用了,即引用计数永远不会为1,故而永远不会被垃圾回收。

标记清除

目前的浏览器都是采用标记清除策略。

标记清除的步骤:

  • 给内存中所有的变量都加上标记
  • 去掉环境中的变量以及变量所引用的其它变量的标记
  • 清除标记的变量

V8的垃圾回收机制

分代策略

根据对象的生命周期不同,采用不同的策略来进行回收。

对象一开始会被分配到新生代,满足一定条件之后就会晋升到老生代。

新生代

里面的对象通常是生命周期比较短的

新生代的内存被平分为2个 semispace,其中一个是活跃的,另一个是闲置的。对象都会被分配到活跃的 semispace中存储。当内存快满时执行垃圾回收,判断对象有没有被引用,是否需要被回收,不回收的对象迁移到闲置内存中,清除需要回收的。垃圾回收完毕,原闲置的 semispace 变为活跃的,原活跃的 semispace 变为闲置的,如此循环。

新生代的垃圾回收过程中,满足如下任一条件的对象将晋升到老生代中:

  1. 已经在两个 semispace 中迁移过一次了
  2. 活跃向闲置迁移时,闲置内存达到25%这个阈值,则后面的对象直接晋升老生代(原因是如果内存占比过高,可能影响后续的内存分配)
老生代

里面的对象通常是生命周期比较长的

老生代中采用 Mark-Sweep 和 Mark-Compact 相结合的方式进行垃圾回收。

  • Mark-Sweep

标记清除。将活跃对象进行标记,然后清除未标记的对象。但是该种方式会出现内存碎片的问题,即内存空间不连续,如果遇到此时需要一块大内存,就可能出现问题。

  • Mark-Compact

标记整理。将活跃对象进行标记,并将活跃对象往内存一侧移动,最后执行清除。这样就能解决内存不连续的问题。

Mack-Compact 需要移动对象,所以效率不是很快。故而v8将两种方式相结合,主要进行 Mark-Sweep,当内存不足以分配给新生代晋升的对象时,再执行 Mark-Compact。

总结

以上便是关于内存的一些总结,参考了不少前辈的文章,如有不对,还望指出!预祝大家年会中大奖~