乐趣区

关于javascript:理解-JavaScript-中的内存管理Memory-Management

平时写 JavaScript 代码时候,咱们很少会思考到内存治理(Memory Management)的问题,因为 JS 曾经帮咱们做了这些事件:

  1. 创立变量(对象,字符串等)时主动进行分配内存。
  2. 对不再应用的变量“主动”开释,也称垃圾回收(garbage collection)。

    然而,了解 JS 的内存治理,能够在咱们遇到内存透露问题的时候及时解决,也有助于咱们写出更高效的代码。

什么是内存

从计算机硬件层面来说,内存是计算机的重要部件之一,它是 CPU 能间接寻址的存储空间。计算机中所有程序的运行都是在内存中进行的。

堆内存和栈内存

栈内存(stack memory:
栈是 JavaScript 用来存储静态数据的数据结构。静态数据是引擎在编译时晓得其大小的数据。在 JavaScript 中,这包含 7 种原始值 (Primitive values)(string, number, boolean, bull, undefined, bigInt, symbol) 和指向对象和函数的援用。

特点:

  1. 栈内存给每个变量调配的内存空间是固定的。也就是说,栈上调配的空间是在编译时由编译器设置的(这个过程也称为“动态内存调配(static memory allocation)”),在程序执行时不会扭转。
  2. 栈内存的大小也是无限的。
  3. 后进先出,存取速度绝对于堆内存,更快。

堆内存 (heap memory):
堆内存是一个不同的存储数据的空间,JavaScript 在这里存储对象和函数。

特点:

  1. 堆内存的大小是在编译过程中动态分配的,这个过程也称“动态内存调配(dynamic memory allocation”
  2. 要留神的是,堆内存和数据结构中的堆齐全是两码事,调配形式倒是相似于链表。具体能够参考 What’s the relationship between “a” heap and “the” heap?
  3. 存取速度绝对比较慢

动态内存调配 & 动态内存调配:

动态内存调配 动态内存调配
编译时晓得内存大小 编译时不晓得内存大小,在程序运行时按需分配
编译时进行调配 程序运行时进行调配
调配栈内存 调配相应的堆内存
FILO(first-in, last-out) 调配没有特定程序

内存生命周期(Memory life cycle)

内存的生命周期和咱们生存中应用工具的状况很像,都是拿,用,还这三个流程。

  1. 按需分配内存(Allocate memory)
    分配内存产生在咱们初始化一个变量的时候。
  2. 应用调配的内存,进行读 / 写操作(Use memory)
  3. 不再须要的时候,开释内存 (Release memory)
    当内存不在须要应用的时候,就要进行开释(Release)然而 JS 不像 C 语言, 能够通过 malloc()free() 函数来进行内存治理,JS 须要依赖“垃圾回收机制”来开释内存
var n = 12; // 给 Number 类型变量 n 调配栈内存
var o = {
  name: "Emon",
  job: "developer",
}; // 给对象类型变量 o 调配堆内存进行存储,并把对 o 的援用存储到栈内存中

垃圾回收(Garbage collection)

垃圾回收的算法次要是依赖于援用的概念。
援用 (Reference):在内存治理的环境中,一个对象如果有拜访另一个对象的权限,叫做一个对象援用另一个对象。援用包含显示援用(explicitly reference) 和隐式援用(implicitly reference)。

var emonObj = {name: "emon", dept: "web"}; // emonObj 对它的原型(prototype)有隐式援用
emonObj.name; // emonObj 对象对 name value 有显示援用

而垃圾回收器就是把咱们不再须要的内存的时候,主动开释它。然而这只能是一个近似的过程,因为“某块内存是否依然须要”的状态是无奈断定(undecidable)的,也就是一个算法在有穷工夫内无奈给出“yes”或“no”答案。
所以判断这个“是否不再须要”的状态,是垃圾回收算法始终在致力思考和优化的问题。

标记 - 革除算法(Mark-and-sweep algorithm)

自从 2012 之后,所有古代浏览器都应用带了标记 - 革除垃圾回收算法。它把“对象是否不再须要”简化为“对象是否能够取得”。
它的原理是:设置一个叫做根的对象(root)(JavaScript 中指的是全局对象)。垃圾回收器将定期从 root 开始,找所有从 root 开始援用的对象,以及援用的援用 … 最终取得所有能够取得的对象和不能取得的对象,并把不能取得的对象所占用的内存给给开释掉。

当然,它也有个限度,那就是无奈从根对象查问到的对象都会被革除。

援用计数垃圾收集(Reference-counting garbage collection)

这是一种最简略是垃圾回收算法,它办法把“对象是否不再须要”简化为“没有其余对象援用它”。如果一个对象是零援用,那么它将会被垃圾回收机制回收。

然而这种算法在循环援用的时候不起作用。因而曾经被废除了。这里咱们不做过多探讨。

内存透露(memory leak)

内存透露是指:

  1. 计算机程序因为某些起因(对存储器配置配置管理失当,忽略或者谬误)造成程序没法是否曾经不再应用的内存。
  2. 配置给对象的存储器无奈被执行程序所拜访。
    内存透露不是指内存在物理上的隐没,而是应用程序调配某段内存之后,因为设计失误,导致在是否该段内存之前就失去了对该段内存的管制,造成内存的节约。

咱们这里会将 4 种常见的内存透露例子,你会发现,如果你了解了幕后产生的事件,这些都是能够轻易防止的。

全局变量(Global variables)

把数据存储在全局变量中可能是最常见的内存透露。如果你用 `var` 关键字申明一个变量,间接定义一个 `function`,或者间接疏忽关键字的时候,浏览器引擎会主动把这个变量加到 `window` 对象外面。
function createPerson() {
  // 以下三个变量,看似定义在 createPerson 办法里,理论寄存在 window 对象里。如果这些数据足够大,则会影响到程序运行速率
  var globalName = "Emon"; // 应用 var 关键字创立的变量
  this.currentPerson = "me"; // 应用 this 创立的变量,这里的 this 指向的是 window
  globalLastName = "Lu"; // 未声明的变量
}

解决办法

  1. 尽量避免定义全局变量,能够通过应用 let,const 关键字来定义变量。
  2. 在严格模式下运行代码来防止这种状况。
  3. 在创立全局变量之后,确保在不应用它的时候开释掉,能够把它设置为null

参考文档

  • JavaScript’s Memory Management Explained
  • How JavaScript works: memory management + how to handle 4 common memory leaks
  • Memory Management
退出移动版