全面分析总结JS内存模型

6,484 阅读9分钟

前言

最近在整体复习一遍现代前端必备的核心知识点,将会整理成一个前端分析总结文章系列。这篇是其中的第二篇,主要是总结下JS底层的内存模型。(另外,此系列文章也可以在语雀专栏——硬核前端系列查看)。

本文首发自迪诺笔记,转载请注明出处😁

一、数据类型与内存

数据类型分类

主要分为两大类:基本数据类型、复杂数据类型,详细分类如下。

基本数据类型 String、Number、Boolean、Null、Undefined、Symbol
复杂数据类型 Object以及所有继承自Object的类型

对于不同的数据类型有不同的内存区域存储数据,基本数据类型直接存储在栈内存,复杂数据类型存储在堆内存。

内存分类

JS中的内存分类与JS引擎有关,在浏览中一般是V8引擎;要进行内存区分主要是为了进行垃圾回收,比如在V8的垃圾回收机制中会根据新生代、老生代内存采用不同回收算法来保证垃圾回收效率。

JS内存空间分为栈(stack)内存和堆(heap)内存,栈内存是栈结构存储基本数据类型和指向堆内存的指针,堆内存存储复杂数据类型

二、变量声明与赋值

核心点总结

  • 变量声明的本质是变量名与栈内存地址进行绑定,不直接与堆内存进行绑定。

  • 声明的基本数据类型会将值存储在栈内存中,声明的复杂数据类型会将值存储在堆内存中并将其在堆中的内存地址作为值存到栈内存中。

  • const声明常量本质是指的是声明的变量名所指向的栈内存地址不可改变,但是栈中对应的值可以改变。

  • 基本数据类型赋值是在栈内存中申请新的内存区域保存值并将其指向的内存地址绑定到原有变量上。

  • 复杂数据类型赋值是在堆内存中申请新的内存区域保存值并将其指向的内存地址作为值在栈内存中申请新的内存区域保存将其在栈中的内存地址绑定到变量上。

详解变量声明与赋值

基本数据类型

let index = 23

基础数据类型直接将值存储在栈内存中,变量绑定到值在栈中对应的地址。

let _index = index

声明另一个变量_index并赋值为index,其实是将_index和index变量绑定到index指向的内存地址。

index = 45

修改变量index的值为基本数据类型,其实是在栈内存中分配内存存储值然后将得到的内存地址绑定到变量index。

图中的Memory指的是栈内存,与下面图中的Stack相同

复杂数据类型

let students = []

复杂数据类型在声明时是在堆内存上分配内存空间存储其值,将分配的堆内存空间地址作为值存储在栈内存上,变量直接绑定的是栈上内存地址。

通过引用来修改复杂数据

let _students = students
_students.push({ name: '小明' })

_status = students 赋值语句只是将两个变量指向同一个栈内存地址,push()语句将在堆内存中分配新空间存储新的数组并将其在堆内存的地址存储到栈中。

更复杂的例子

let obj = { name: '小明' }
let arr = []
arr = [ obj ]
obj = null

内存模型示意图如下:

[obj]属于复杂类型中引用复杂类型是通过指针引用处理,虽然通过obj=null来清除了obj对于对象{index:'小明'}的绑定,但是arr对该对象任然存在引用。

详解常量声明与赋值

声明基本数据类型为常量过程与基本数据类型的声明过程相同

const index = 1
index = 100 // TypeError: Assignment to constant variable.

对声明为基本数据类型的常量进行赋值会发生结果

在将index变量绑定到新产生的内存地址时报错:不允许修改常量绑定的内存地址。

声明复杂数据类型为常量过程与基本数据类型的声明过程相同。

const students = []
students = [{ name: '小红' }]

对声明为复杂数据类型的常量进行赋值会产生如下结果

在将students变量绑定到新产生的内存地址时报错:不允许修改常量绑定的内存地址。

深复制与浅复制

上面说的复杂数据类型通过指针指向了同一块堆内存空间,深、浅复制主要区别就在于复制值的时候是否新分配堆内存空间来保存原值的拷贝。

对于对象或数组类型,当我们将 a 赋值给 b,然后更改 b 中的属性,a 也会随着变化。 也就是说 a 和 b 指向了同一块内存,所以修改其中任意的值,另一个值都会随之变化,这就是浅复制(拷贝)

深复制(拷贝)则是在上述 a 赋值给 b 过程分配了新堆内存空间来存储拷贝的值,同时在存在复杂数据类型的嵌套属性(递归遍历)也要用同样方式处理,最后复制出来的新数据对象下的任意层级的复杂对象都有新的堆内存存储相应的值。

此处不再赘述,可以参考此文

三、垃圾回收

有内存就必然有垃圾回收GC),JS中栈内存多数是在函数执行时使用(根据函数调用顺序也叫做调用栈),函数执行完后即开始栈内存的垃圾回收。堆内存由于存在多个栈内存中的指针指向它以及堆内存较大等原因,需要采用特定的垃圾回收算法处理。

垃圾回收的关键在于如何判断内存已经不再使用然后将其释放掉

引用计数算法

主要是IE等旧浏览器在采用,通过计数器分析变量的引用次数,清除没有引用到的变量。对于存在循环引用的情况则无法处理,比如:

function cycle() {
var o1 = {}
var o2 = {}
o1.a = o2
o2.a = o1
return "Cycle reference!"
}
cycle()

其中 o1 引用了 o2,o2 引用了 o1,在cycle函数执行完 o1,o2 都没有再次引用到,但是引用计数算法判断两者都存在引用。

Scavenge算法

用于V8中新生代内存,将新生代内存一分为二:From和To,在From与To之间转换的过程中完成垃圾回收。

标记清除算法

早期V8中堆内存采用的一种清除算法,全局扫描堆内存找出未使用到的对象进行标记并清除,由于未进行内存整理会存在内存碎片。

标记整理算法

全局扫描堆内存找出未使用到的对象边整理边清除,解决了标记清除算法导致的内存碎片问题。

增量式清除、整理

堆内存大小一般较大,在采用前几种算法进行垃圾回收时需要扫描全堆,导致JS执行逻辑长时间暂停。增量式清除、整理是将标记清除或标记整理拆分为一个步进,轮流执行JS逻辑和一个步进,最大程度较少JS执行逻辑暂停时间

🌰实例分析

以在浏览器控制台运行下面这段代码为例(暂时不考虑ES6语法兼容性)。

const fn = (arr) => {
const _arr = arr
return _arr.join()
}
const index = 0
const array = new Array(20).fill(1)
fn(array)

变量声明(依次执行)

依次执行:堆内存储函数fn、栈内存存储常量整数index、堆内存存储数组array

调用函数(函数调用栈)

依次执行:变量arr、_arr指向数组array、存储join方法返回的字符串到栈内存

清除函数调用栈

函数调用栈中的变量arr、_arr属于函数作用域,此时已经不可访问将被清除。

清除堆栈内存

整个JS逻辑执行完成,函数fn、常量index、数组array、函数fn返回的字符串都将会清除。

若在浏览器控制台中,执行完上述JS逻辑并未退出控制台,上述清除堆栈内存将在关闭控制台后执行。

四、常见问题分析

🌰闭包导致内存泄露

闭包就是通过返回一个函数间接地使外部有机会访问到函数内部的变量,扩展了JS中函数作用域的范围。

创建闭包方法如下:

const generateFn = () => {
const obj = { index: 1 }
return () => {
return obj
}
}
const obj2 = generateFn()() // 此时obj2就指向了上面定义的obj

下面用法会导致内存泄露: i

window.fn = generateFn() // 返回的函数绑定至了全局,没有主动清除

generateFn生成了一个引用obj的函数,同时将其绑定至了全局对象window,导致fn不会被回收,而fn引用了obj使obj也不会被回收,于是产生了内存泄露。

🌰WeakSet、WeakMap的弱引用

WeakSet和WeakMap是ES6中两种新的数据结构,它们对于值的引用都不计入垃圾回收机制。WeakSet只能存储不重复的对象,WeakMap只能以对象为key来存储key-value对。对应对象在外部变为不可访问时,其对应的存储记录也将自行丢失。

WeakMap分析

通过分析下面这段代码来说明其弱引用的特性。

let obj = { index: 0 }
let vs = new WeakSet()
vs.add(obj)
obj = null // 清除obj使在进行GC时清除{ index: 0 }对象

将obj对象添加进WeakSet实例中,此时可以通过obj和vs变量来访问到obj对象,通过obj=null清除对象,此时vs中的obj对象的引用也会自动清除

在GC完成之后(这里直接console.log打印还是可以看到obj对象的,因为GC没有完成),可以看到vs的items是空的。

另外,在研读React源码的过程中发现其中DOMEventListenerMap.js中有对WeakMap实际应用,下篇文章将会深入研究一下WeakMap、WeakSet等实际的应用。

写在最后

既然看到这里了不妨点个赞鼓励下作者呗 :)

作者博客:blog.lessing.online

作者github:github.com/johniexu

【全面分析总结前端系列】

参考文章