1、内存办理

程序的运转需求占用内存,JavaScript 是在创立变量(目标,字符串等)时主动进行了内存分配,而且在不运用它们时“主动”开释。开释的进程称为废物收回(Garbage Collection,简称 GC)。这个“主动”是混乱的本源,并让开发者过错的感觉他们能够不关心内存办理,这个“主动”并不足够智能,那些很占空间的值一旦不再用到,有必要检查是否还存在对它们的引证。假如是的话,就需求手动免除引证。

2、内存分配

在js中,将数据分为原始数据类型引证数据类型,分别存储在栈和堆中,那么,栈内存和堆内存有什么区别呢?

堆内存(Heap Memory)

  • 动态分配:堆内存是动态分配的,一般用于存储运用程序中的目标和其他复杂数据结构。分配和开释的次序不固定。
  • 废物收回:废物收回器会定期扫描堆内存,查找那些不再被任何变量或目标引证的内存块,然后开释这些内存以供再次运用。
  • 大局拜访:堆内存中的目标能够在运用程序的任何地方被拜访。

栈内存(Stack Memory)

  • 静态分配:栈内存是静态或主动分配的,首要用于存储局部变量和函数调用的上下文。分配和开释的次序是严格定义的,遵循后进先出(LIFO)的准则。
  • 无需废物收回:当函数履行结束,其作用域中的局部变量会主动被铲除。因而,栈内存不需求废物收回器来办理。
  • 作用域限制:栈内存中的变量只能在其声明的函数内部拜访。

因为栈内存的分配和收回是主动和确认的,因而废物收回器首要重视堆内存的办理。在栈上,当函数调用回来时,它的栈帧(包含一切局部变量)就会被移除,这一进程是由 CPU 的调用仓库机制主动办理的,不需求废物收回器参与。

但是,需求留意的是,虽然栈内存的局部变量在函数回来后会被铲除,但假如这些局部变量持有对堆内存中目标的引证,那么这些目标的生命周期或许会超过函数调用本身。在这种状况下,只有当没有任何引证指向堆内存中的目标时,废物收回器才会收回该目标占用的内存。

3、什么样的内存称为废物?

程序不再运用的内存(如:不再被引证的目标)或一些不可达的目标(不能从根上拜访)

4、如何辨认内存走漏?

内存走漏一般指的是程序中已分配的内存因为某些原因没有被开释或无法被开释,长期运转后会导致运用程序运用的内存不断增加,终究或许耗尽系统资源,导致程序功能下降乃至崩溃。在 JavaScript 环境中,内存走漏一般发生在堆内存中,因为这是大部分动态分配内存的地方,比方目标、数组和函数闭包等。

经验法则是:假如接连五次废物收回之后,内存占用一次比一次大,就有内存走漏。这就要求实时检查内存占用。

1)浏览器

Chrome 浏览器检查内存占用,假如内存占用基本平稳,挨近水平,就阐明不存在内存走漏

JS的内存走漏与废物收回机制

反之则是内存走漏

JS的内存走漏与废物收回机制

2)命令行:运用 Node 提供的process.memoryUsage办法

该办法会回来一个目标:

  • rss(resident set size):一切内存占用,包含指令区和仓库。

  • heapTotal:”堆”占用的内存,包含用到的和没用到的。

  • heapUsed:V8 引擎为 JavaScript 分配的堆内存中现已运用的部分

  • external: V8 引擎内部的 C++ 目标占用的内存。

    判断内存走漏我们一般是以heapUsed为准,需求留意的是,heapUsed是评估 JavaScript 内存走漏的一个重要目标,但它不是唯一的目标。关于内存走漏的全面诊断,或许还需求结合其他东西和办法,比方剖析(profiling)东西、内存快照(memory snapshots)和堆剖析(heap analysis)等。

5、废物收回的触发时机?

废物收回能够周期性地运转,也或许在到达内存分配阈值时触发。因为废物收回或许会暂停程序履行(称为“中止”),现代的废物收回器会尽量优化这些中止,使其对程序的影响最小。

6、废物收回

常见的办法是以下两种:

1)、符号-铲除 (Mark-and-Sweep)

这是最常见的废物收回算法。它的基本思路分为两个阶段:

符号阶段:废物收回器会从根(一般是大局目标)开始,遍历一切从根开始的引证,递归符号一切可达的(reachable)目标(在运用中依然能够拜访到的目标)。

铲除阶段:废物收回器接着会查找一切未被符号的目标,并开释它们的内存。因为这些目标不可达,它们被认为是“废物”。

缺陷-内存碎片:被开释的内存方位是不变的,它不会从头排序内存空间,而我们在插入引证类型的数据时它们需求是接连的内存空间,故对内存空间的利用率并不高

符号收拾(Mark-Compact)算法(旨在处理符号铲除算法带来的内存碎片问题),在收拾阶段,它会将一切活泼的目标向一端移动,然后清理掉未符号目标所占用的空间,并消除内存中的碎片。在收拾进程中,活泼目标的引证地址或许会发生变化,所以需求更新原来指向这些目标的引证,确保它们指向新的地址。由此我们会发现,虽然移动的操作提高了内存分配的效率,但移动目标并更新引证这一进程或许会导致较大的功能开支。因为收拾阶段要暂停程序履行(Stop-The-World),因而或许会影响到程序的响应时间和吞吐量,尤其是在堆内存比较大的状况下。

现代废物收回器一般运用多种算法组合来办理内存,例如,将堆内存分红几个区域(如新生代、老时代),并针对不同的区域运用不同的废物收回战略,以到达高效和低推迟的废物收回作用。符号收拾算法一般用于老时代废物收回,这是因为老时代中目标的存活率较高,移动存活目标并收拾内存能够有效地削减碎片,为新目标的分配提供更大的接连空间。

2)、引证计数 (Reference Counting)

这是另一种废物收回算法,其思路相对简单:言语引擎有一张”引证表”,保存了内存里面一切的资源(一般是各种值)的引证次数。假如一个值的引证次数是0,就表明这个值不再用到了,因而能够将这块内存开释

但存在这样的状况:比方一个值不被需求,引证数却不为0,废物收回机制无法开释这块内存,然后导致内存走漏

const arr = [1, 2, 3, 4];
console.log('hello world');

以上,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引证,因而引证次数为1。虽然后边的代码没有用到arr,但它依然会持续占用内存。

故我们需求手动开释,将arr重置为null,免除arr[1, 2, 3, 4]的引证,引证次数变成了0,这块内存就能够被废物收回机制开释了。

let arr = [1, 2, 3, 4];
console.log('hello world');
arr = null; // 手动免除

因而,并不是说有了废物收回机制,程序员就无需重视内存占用了~

丧命的问题: 循环引证。假如两个或两个以上的目标相互引证,但它们都不再被其他活泼目标或根引证,那么它们的引证次数永远不会到达0,即便它们实际上是不可拜访的。

function createExample() {
    var objectA = {}; 
    var objectB = {};
    // 创立循环引证
    objectA.a = objectB; 
    objectB.a = objectA;
} 
createExample();

函数履行结束,而且外部都不存在对 objectA 和 objectB 的引证了,它们的引证计数依然为1,因为它们相互引证。故在朴实的引证计数废物收回机制中,这将导致 objectA 和 objectB 永远不会被收回。所以现代浏览器不会运用朴实的引证计数法

7、WeakMap

假如能有一种办法,在新建引证的时分就声明,哪些引证有必要手动铲除,哪些引证能够忽略不计,当其他引证消失今后,废物收回机制就能够开释内存。那么就能削减因为程序员疏忽而导致的内存走漏了。ES6 考虑到这一点,推出了两种新的数据结构:WeakSetWeakMap

WeakMap是一种键值对的集合,其中的键有必要是目标或非大局注册的符号,且值能够是恣意的JavaScript 类型,而且不会创立对它的键的强引证。换句话说,一个目标作为WeakMap的键存在,不会阻挠该目标被废物收回。一旦一个目标作为键被收回,那么在WeakMap中相应的值便成为了进行废物收回的候选目标,只需它们没有其他的引证存在。

WeakMap答应将数据与目标相关联,而不阻挠键目标被废物收回,即便值引证了键

参考文献:

MDN-内存办理

阮一峰-JavaScript 内存走漏教程

MDN-WeakMap