平时写 JavaScript 代码时候,咱们很少会思考到内存治理(Memory Management)的问题,因为 JS 曾经帮咱们做了这些事件:
- 创立变量(对象,字符串等)时主动进行分配内存。
-
对不再应用的变量“主动”开释,也称垃圾回收(garbage collection)。
然而,了解 JS 的内存治理,能够在咱们遇到内存透露问题的时候及时解决,也有助于咱们写出更高效的代码。
什么是内存
从计算机硬件层面来说,内存是计算机的重要部件之一,它是 CPU 能间接寻址的存储空间。计算机中所有程序的运行都是在内存中进行的。
堆内存和栈内存
栈内存(stack memory:
栈是 JavaScript 用来存储静态数据的数据结构。静态数据是引擎在编译时晓得其大小的数据。在 JavaScript 中,这包含 7 种原始值 (Primitive values)(string, number, boolean, bull, undefined, bigInt, symbol) 和指向对象和函数的援用。
特点:
- 栈内存给每个变量调配的内存空间是固定的。也就是说,栈上调配的空间是在编译时由编译器设置的(这个过程也称为“动态内存调配(static memory allocation)”),在程序执行时不会扭转。
- 栈内存的大小也是无限的。
- 后进先出,存取速度绝对于堆内存,更快。
堆内存 (heap memory):
堆内存是一个不同的存储数据的空间,JavaScript 在这里存储对象和函数。
特点:
- 堆内存的大小是在编译过程中动态分配的,这个过程也称“动态内存调配(dynamic memory allocation”
- 要留神的是,堆内存和数据结构中的堆齐全是两码事,调配形式倒是相似于链表。具体能够参考 What’s the relationship between “a” heap and “the” heap?
- 存取速度绝对比较慢
动态内存调配 & 动态内存调配:
动态内存调配 | 动态内存调配 |
---|---|
编译时晓得内存大小 | 编译时不晓得内存大小,在程序运行时按需分配 |
编译时进行调配 | 程序运行时进行调配 |
调配栈内存 | 调配相应的堆内存 |
FILO(first-in, last-out) | 调配没有特定程序 |
内存生命周期(Memory life cycle)
内存的生命周期和咱们生存中应用工具的状况很像,都是拿,用,还这三个流程。
- 按需分配内存(Allocate memory)
分配内存产生在咱们初始化一个变量的时候。 - 应用调配的内存,进行读 / 写操作(Use memory)
- 不再须要的时候,开释内存 (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)
内存透露是指:
- 计算机程序因为某些起因(对存储器配置配置管理失当,忽略或者谬误)造成程序没法是否曾经不再应用的内存。
- 配置给对象的存储器无奈被执行程序所拜访。
内存透露不是指内存在物理上的隐没,而是应用程序调配某段内存之后,因为设计失误,导致在是否该段内存之前就失去了对该段内存的管制,造成内存的节约。
咱们这里会将 4 种常见的内存透露例子,你会发现,如果你了解了幕后产生的事件,这些都是能够轻易防止的。
全局变量(Global variables)
把数据存储在全局变量中可能是最常见的内存透露。如果你用 `var` 关键字申明一个变量,间接定义一个 `function`,或者间接疏忽关键字的时候,浏览器引擎会主动把这个变量加到 `window` 对象外面。
function createPerson() {
// 以下三个变量,看似定义在 createPerson 办法里,理论寄存在 window 对象里。如果这些数据足够大,则会影响到程序运行速率
var globalName = "Emon"; // 应用 var 关键字创立的变量
this.currentPerson = "me"; // 应用 this 创立的变量,这里的 this 指向的是 window
globalLastName = "Lu"; // 未声明的变量
}
解决办法
- 尽量避免定义全局变量,能够通过应用
let
,const
关键字来定义变量。 - 在严格模式下运行代码来防止这种状况。
- 在创立全局变量之后,确保在不应用它的时候开释掉,能够把它设置为
null
。
参考文档
- JavaScript’s Memory Management Explained
- How JavaScript works: memory management + how to handle 4 common memory leaks
- Memory Management