前言
讲真,这两个概念很容易被混为一谈。
正文
一、内存
在 JavaScript 中,没有像 C 语言等提供有内存管理接口,JavaScript 是在创建变量时自动进行分配内存,并且在不使用它们时“自动”释放。释放的过程被称为“垃圾回收”。
这个“自动”就是混乱的根源,并让 JavaScript 开发者错误地认为他们可以不用关心内存管理。
内存的生命周期
不管什么程序语言,内存的生命周期基本是一致的:
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时,将其释放/归还
所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像 JavaScript 这些高级语言中,大部分都是隐含的。
内存管理的难题
大多数内存管理的问题都在“当内存不需要使用时释放”这个阶段。最困难的就是如何界定并找到“哪些被分配的内存确实不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。
垃圾回收(Garbage Collection,GC)
由于一些内存“不再需要”的问题无法判定,因此,垃圾回收实现只能有限制的解决一般问题。
- 引用计数算法
最初的垃圾回收算法,它把“对象是否不再需要”简化为“对象有没有其他对象引用到它”。如果没有引用指向改对象(零引用),对象将被垃圾回收机制回收。但是这个算法有个限制,无法处理循环引用的情况。
关于“引用”的概念,一个对象如果有访问另一个对象的权限(显式或隐式),叫做一个对象引用另一个对象。 - 标记清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫做根对象(在 Javascript 里是globalThis
对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
它解决了早期算法无法处理循环引用的问题,但还是有一个限制:那些无法从根对象查询到的对象都将被清除(实际中很少会碰到这种情况)。
从 2012 年起,所有现代浏览器都使用了标记清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
所以,通常我们不会在全局作用域下进行过多的变量或函数声明,更推荐将它们放在立即执行函数表达式(IIFE)内进行声明。否则它们将无法被垃圾回收,即在程序的生命周期内一直存在。
这一小节提到的“对象”,不仅特指 JavaScript 对象,还包括函数作用域、全局作用域。
二、内存泄露、溢出
区别
这两个概念是存在区别的。
- 内存溢出(Out of Memory)
当系统无法提供应用程序所需内存时,会导致应用程序抛出内存溢出的错误。 - 内存泄露(Memory Leak)
应用程序中一些被分配的内存,在使用完之后,没有及时被垃圾回收器进行回收(释放),导致一部分无效的内存被占用着。
当内存泄露积累到一定程度,就会发生内存溢出。而内存溢出导致的结果是应用程序被杀死。
场景
- 内存溢出
const obj = {} for (let i = 0; i < 10000; i++) { obj[i] = new Array(1000000) } // 将会崩溃
- 内存泄露
- 意外的全局变量
- 闭包
- setInterval 没有及时清除
- DOM 引用未移除
- ...
未完待续...