JavaScript内存管理
在软件中管理和协调内存的做法称为内存管理。它确保内存块得到正确的管理和分配,以便当前正在运行的应用程序和其他进程拥有完成其任务所需的内存。对于所有编程语言来说,内存生命周期的三个阶段或部分都相同:
- 内存分配:操作系统在程序执行过程中根据需要分配内存;
- 使用内存:程序使用先前分配的内存,对内存执行
read
和write
操作; - 释放内存:任务完成后,分配的内存将被释放并释放。

内存管理
内存管理(Memory management)是应用于计算机内存的资源管理的一种形式。内存管理的基本要求是提供方法,根据程序的请求动态分配部分内存,并在不再需要时将其释放以供重用。这对于任何先进的计算机系统来说都是至关重要的,因为在任何时候都可能有多个进程正在进行。
大多数时候,作为一名 JavaScript 开发人员,即使对内存管理一无所知,也可以过得很好。JavaScript 不需要 C 语言那样调用 malloc()
和 free()
底层接口来管理内存,毕竟,JavaScript 引擎会为处理这个问题。
内存分配
为了不让程序员费心分配内存,JavaScript 引擎在创建变量、函数或任何想到的东西时分配内存。
// 定义变量时就完成了内存分配
var num = 1 // 给数值变量分配内存
var str = 'hello' // 给字符串分配内存
var obj = { name: 'John' } // 给对象及其包含的值分配内存
var arr = [1, 2, 3] // 给数组及其包含的值分配内存(就像对象一样)
function foo() {} // 给函数(可调用的对象)分配内存
dom.addEventListener('click', foo) // 函数表达式也能分配一个对象
// 函数调用结果是分配对象内存
var date = new Date() // 分配一个 Date 对象
var ele = document.createElement('div') // 分配一个 DOM 元素
// 对象方法分配新变量或者新对象
var arr2 = arr.concat([4, 5]) // 分配一个新数组
使用内存
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。所有语言,使用内存都是明确的。内存分配、释放内存在底层语言是明确的,但是在高级语言中,大部分都是隐式。
释放内存
当内存不再使用时释放,大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”,它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。JavaScript 自动分配内存,并且在不使用它们时自动释放,释放的过程称为垃圾回收。

内存分配
在 JavaScript 中,有两种主要类型的内存:堆栈和堆。两者都由 JavaScript 引擎管理,都用于存储运行时数据。区别在于速度和大小。堆越大,速度越慢,堆栈越小,速度越快。引擎如何知道使用哪一个?经验法则是:如果引擎不确定它使用堆的大小。如果引擎可以预先计算大小,则它使用堆栈。
在大多数现代计算机系统中,每个线程都有一个保留的内存区域,称为堆栈(Stack),计算架构中的堆栈是内存区域,其中数据以后进先出 (LIFO) 方式添加或删除。堆(Heap)是可用于为程序动态分配内存区域(“块”)的内存区域。通过从称为堆或空闲存储的大型内存池中分配部分来满足内存请求。
堆栈存储器存储静态数据,即数据大小在编译时已知,分配固定数量的内存。这包括原始类型(number
、boolean
、string
、Symbol
、BigInt
、null
和 undefined
)和对象引用(实际对象存储在堆上)。堆栈内存的限制因浏览器而异。需要注意的一些重要事项:
- 基本类型直接存储在堆栈中,为了跟踪当前的内存位置,有一个称为堆栈指针的特殊处理器寄存器,这些是指向位于堆内存中的对象/函数的指针。
- 所有局部变量(包括函数的参数和返回值)都存储在堆栈上的函数框架块中。当退出函数时,与该函数关联的所有内容都会从堆栈中清除。
- 开发人员无法控制堆栈内存,它由 JavaScript 引擎管理。
当堆栈超过其最大大小时,就会发生堆栈溢出。当存在无限循环或不返回的递归函数时,就会发生这种情况。
与堆栈内存相反,堆内存是无序的,主要用于存储对象、数组和函数等引用类型,内存是动态分配的,堆中的数据通常称为动态数据。JavaScript 堆不像堆栈那样分配固定数量的内存,而是在运行时分配更多空间,即大小在运行时已知,并且其对象内存没有限制。
在计算机科学中, 动态内存分配(Dynamic memory allocation)又称为堆内存分配,是指计算机程序在运行期中分配使用内存。它可以当成是一种分配有限内存资源所有权的方法。
对象可以通过两种方式保存内存:
- 直接由对象本身。
- 通过隐式保存对其他对象的引用,从而防止垃圾收集器自动处置这些对象。
垃圾回收
在计算机科学中,垃圾收集(Garbage collection)是自动内存管理的一种形式。垃圾收集器尝试回收由程序分配但不再被引用的内存;这样的内存被称为垃圾。
垃圾收集使程序员无需进行手动内存管理,程序员可以指定要取消分配并返回到内存系统的对象以及何时执行此操作。其他类似的技术包括堆栈分配、区域推断和内存所有权及其组合。垃圾收集可能会占用程序总处理时间的很大一部分,并因此影响性能。
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个 Javascript 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在计算机科学中,引用是指引用内存中其他位置的对象的数据类型,用于构造各种数据结构,例如链表。通常,引用是使程序能够直接访问特定数据项的值。
引用计数垃圾收集
在计算机科学中,引用计数(Reference counting)是一种存储资源(例如对象、内存块、磁盘空间等)的引用、指针或句柄数量的编程技术。
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。引用计数垃圾收集是指每个对象都有对其引用数量的计数。垃圾通过引用计数为零来识别。当创建对对象的引用时,对象的引用计数会增加;而当引用被销毁时,对象的引用计数会减少。当计数达到零时,对象的内存被回收。
let student = {
name: 'Tom',
age: 20
sports: ['basketball', 'football']
}
let otherStudent = student
const sports = student.sports
student = null
otherStudent = null
首先,两段内存被分配。“数组内存”分配给 sports
,作为对象的属性被引用,“对象内存”分配给变量 student
。很显然,没有一个可以被垃圾回收。接着,student
赋值给变量 otherStudent
,“对象内存”又被 otherStudent
引用,计数增加。student.sports
赋值给变量 sports
,“数组内存”又被 sports
引用,计数增加。
最后,student
和 otherStudent
被赋值为 null
,计数减少。虽然“对象内存”已经是零引用了,可以被垃圾回收了,但是它的属性 sports
的对象还在被 sports
变量引用,所以还不能回收。
引用计数垃圾收集的限制:循环引用。
该算法有个限制:无法处理循环引用的事例。当两个对象引用自身时,引用计数算法就会出现问题。简单来说,如果存在循环引用,该算法就无法确定自由对象。
let person = { name: 'Tom' }
let employee = { id: 100 }
person.employee = employee
employee.person = person
person = null
employee = null
两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
employee
被分配给 person
, person
被分配给 employee
,因此这些对象相互引用。这些对象赋值为 null
后,它们将失去内存上的引用,但对象仍然保留在内存中。引用算法无法释放这些对象,因为它们具有引用。循环引用问题可以使用标记和清除算法来解决。
标记和清除算法
在计算机编程中,跟踪垃圾收集(Tracing garbage collection)是自动内存管理的一种形式,它包括通过跟踪某些“根”对象的引用链可以访问哪些对象来确定应释放哪些对象(“垃圾收集”),并考虑其余的作为“垃圾”并收集它们。
此算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
- 开始阶段:一旦开始,它假设所有对象都是无法访问的(无法访问的对象是无法通过所谓的根的引用进行遍历来访问的任何对象)。
- 标记阶段:然后开始从根(通过引用)开始实际的树遍历。途中发现的每个对象都被标记为可达。
- 扫描阶段:遍历完成后,所有无法到达的对象都将被消除。
var employee = {
person: { name: 'Tom' },
company: { name: 'Google' }
}
let car = { model: 'BMW', employee }
window
对象可以访问 employee
对象,company
和 person
对象可以通过 employee
对象访问,因此它们最终可以通过 window
对象访问。但是, car
对象和与其连接的其他对象无法通过 window
对象访问。因此,算法会标记这些对象,然后释放它们使用的内存。
标记和清除算法的限制:那些无法从根对象查询到的对象都将被清除。
不过,一种权衡是使用 var
声明的全局变量。当使用 var
声明变量时,JavaScript 引擎会将它们附加到 window
对象。现在,由于这些变量始终可以被 window
对象访问,因此垃圾收集器永远不会删除它们。
转载自:https://juejin.cn/post/7268867293755637779